feat(web): share project actions via t3.json#4363
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
| const scripts = Array.isArray(rawProjectFile.scripts) | ||
| ? [...rawProjectFile.scripts] | ||
| : [...decodedScripts]; | ||
| if (existingIndex === -1) { | ||
| scripts.push(nextFileScript); | ||
| } else { | ||
| scripts[existingIndex] = nextFileScript; | ||
| } |
There was a problem hiding this comment.
🟡 Medium src/t3ProjectFileScripts.ts:76
When upsertT3ProjectFileScript updates an existing action, scripts[existingIndex] = nextFileScript replaces the entire raw script object wholesale, silently deleting any forward-compatible or tool-specific fields the user had on that entry. This defeats the function's purpose of preserving unknown raw JSON fields. When rawProjectFile.scripts is the source list and the existing raw entry is an object, spread the existing entry before nextFileScript so extra keys are retained.
const scripts = Array.isArray(rawProjectFile.scripts)
? [...rawProjectFile.scripts]
: [...decodedScripts];
if (existingIndex === -1) {
scripts.push(nextFileScript);
} else {
- scripts[existingIndex] = nextFileScript;
+ const existingRaw = scripts[existingIndex];
+ scripts[existingIndex] =
+ existingRaw && typeof existingRaw === "object" && !Array.isArray(existingRaw)
+ ? { ...(existingRaw as Record<string, unknown>), ...nextFileScript }
+ : nextFileScript;
}🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/t3ProjectFileScripts.ts around lines 76-83:
When `upsertT3ProjectFileScript` updates an existing action, `scripts[existingIndex] = nextFileScript` replaces the entire raw script object wholesale, silently deleting any forward-compatible or tool-specific fields the user had on that entry. This defeats the function's purpose of preserving unknown raw JSON fields. When `rawProjectFile.scripts` is the source list and the existing raw entry is an object, spread the existing entry before `nextFileScript` so extra keys are retained.
| const existingIndex = decodedScripts.findIndex( | ||
| (fileScript) => | ||
| (input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) || | ||
| isSameScript(fileScript, input.script), | ||
| ); |
There was a problem hiding this comment.
🟠 High src/t3ProjectFileScripts.ts:67
upsertT3ProjectFileScript can overwrite the wrong action when editing a shared action. If the action being edited appears later in scripts but an earlier unrelated action already shares the new name or command, findIndex matches the earlier action and overwrites it, leaving the original edited action stale in the file. The previousScript match is not prioritized over the new-name match because both checks run in the same findIndex pass. Search for a previousScript match first in a separate pass, and only fall back to matching by the new name or command if no previous match is found.
| const existingIndex = decodedScripts.findIndex( | |
| (fileScript) => | |
| (input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) || | |
| isSameScript(fileScript, input.script), | |
| ); | |
| const matchPrevious = (fileScript: T3ProjectFileScript) => | |
| input.previousScript !== undefined && isSameScript(fileScript, input.previousScript); | |
| const matchCurrent = (fileScript: T3ProjectFileScript) => | |
| isSameScript(fileScript, input.script); | |
| const previousIndex = decodedScripts.findIndex(matchPrevious); | |
| const existingIndex = | |
| previousIndex !== -1 | |
| ? previousIndex | |
| : decodedScripts.findIndex(matchCurrent); |
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/t3ProjectFileScripts.ts around lines 67-71:
`upsertT3ProjectFileScript` can overwrite the wrong action when editing a shared action. If the action being edited appears later in `scripts` but an earlier unrelated action already shares the new name or command, `findIndex` matches the earlier action and overwrites it, leaving the original edited action stale in the file. The `previousScript` match is not prioritized over the new-name match because both checks run in the same `findIndex` pass. Search for a `previousScript` match first in a separate pass, and only fall back to matching by the new name or command if no previous match is found.
| return appAtomRegistry.get(optimisticFileAtom(environmentId, cwd, relativePath))?.data ?? null; | ||
| } | ||
|
|
||
| export function getProjectFileQueryData( |
There was a problem hiding this comment.
🟠 High files/projectFilesQueryState.ts:71
getProjectFileQueryData returns null whenever the AsyncResult has no value — including while the read is still in-flight or after a transient read failure. Callers treat null as "file does not exist" and proceed to create fresh contents and write them, so an existing t3.json can be overwritten and project settings/actions lost if the user saves before the initial read completes or after a transient read failure. The function conflates "no value yet" with "file does not exist" because it only checks Option.getOrNull(AsyncResult.value(result)) and ignores the AsyncResult state. Consider distinguishing pending/failed states from a confirmed empty result so callers don't write over a file they haven't successfully read.
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/components/files/projectFilesQueryState.ts around line 71:
`getProjectFileQueryData` returns `null` whenever the `AsyncResult` has no value — including while the read is still in-flight or after a transient read failure. Callers treat `null` as "file does not exist" and proceed to create fresh contents and write them, so an existing `t3.json` can be overwritten and project settings/actions lost if the user saves before the initial read completes or after a transient read failure. The function conflates "no value yet" with "file does not exist" because it only checks `Option.getOrNull(AsyncResult.value(result))` and ignores the `AsyncResult` state. Consider distinguishing pending/failed states from a confirmed empty result so callers don't write over a file they haven't successfully read.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit d98df4f. Configure here.
| contents: existingContents, | ||
| script: input.script, | ||
| ...(input.previousScript ? { previousScript: input.previousScript } : {}), | ||
| }); |
There was a problem hiding this comment.
Share skips unread t3.json
High Severity
Sharing a project script can overwrite the t3.json file. This happens because getProjectFileQueryData returns null if the file read query is pending or failed. upsertT3ProjectFileScript then interprets this null as an empty file, leading to a new, minimal t3.json being written that discards existing shared actions and settings.
Reviewed by Cursor Bugbot for commit d98df4f. Configure here.
| (fileScript) => | ||
| (input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) || | ||
| isSameScript(fileScript, input.script), | ||
| ); |
There was a problem hiding this comment.
Edited share hits wrong row
High Severity
The findIndex logic in upsertT3ProjectFileScript can incorrectly identify the script to update. When matching, isSameScript broadly checks for either name or command equality. This can lead to an unrelated, earlier script being overwritten if it matches the new name or command, leaving the intended script stale and corrupting t3.json.
Reviewed by Cursor Bugbot for commit d98df4f. Configure here.
ApprovabilityVerdict: Needs human review 3 blocking correctness issues found. New feature introducing project action sharing via t3.json. Multiple high-severity unresolved review comments identify potential data loss bugs: file reads returning null during pending state could overwrite existing t3.json, and the script matching logic could update the wrong action. You can customize Macroscope's approvability policy. Learn more. |


What Changed
t3.jsonwith the published schema when the first action is sharedt3.jsonwithout duplicating matching actions or dropping unrelated project settingst3.jsoncannot be updatedWhy
t3.jsonalready lets teammates import repository-defined actions, but sharing a newly configured action currently requires manually editing JSON. This keeps personal actions local by default while making the existing checked-in sharing workflow available directly from the action form.This builds on #4317.
UI Changes
Before: Actions created in the dialog were stored only in the local project configuration; sharing required a manual
t3.jsonedit.After: The same dialog includes an off-by-default sharing switch with explanatory copy. Enabling it writes the saved action to the repository's
t3.json.The requester manually verified the complete add-and-share flow in the live development app.
Verification
pnpm --filter @t3tools/web typecheckpnpm exec vp lint apps/web/src/components/ChatView.tsx apps/web/src/components/ProjectScriptsControl.tsx apps/web/src/components/files/projectFilesQueryState.ts apps/web/src/t3ProjectFileScripts.ts apps/web/src/t3ProjectFileScripts.test.tspnpm exec vp test run apps/web/src/t3ProjectFileScripts.test.ts apps/web/src/components/files/projectFilesQueryState.test.ts(6 tests)Checklist
Note
Medium Risk
Opt-in writes to version-controlled
t3.jsonin the workspace; validation and partial-failure handling limit blast radius, but teammates can still pick up unintended shared actions if misused.Overview
Adds an off-by-default “Share with everyone on this project” switch to the add/edit action dialog so a saved action can be written into the repo’s checked-in
t3.json, not only local project config.After a successful local save, optional sharing reads current
t3.json(via newgetProjectFileQueryData), merges the action withupsertT3ProjectFileScript(create file, append, or update by name/command; keeps unrelated keys; 50-script cap; invalid JSON errors), then persists throughwriteProjectFile. Keybindings stay local. If sharing fails, the user gets a non-blocking toast (“Action saved, but could not be shared”).Includes unit tests for the merge helper.
Reviewed by Cursor Bugbot for commit d98df4f. Bugbot is set up for automated code reviews on this repo. Configure here.
Note
Share project actions via t3.json when saving or updating scripts
ProjectScriptsControl, exposing a newshareWithProjectflag onNewProjectScriptInput.shareWithProjectis true,ChatViewContentwrites or updates the action in the project'st3.jsonfile after saving, using the newupsertT3ProjectFileScriptutility int3ProjectFileScripts.ts.upsertT3ProjectFileScriptpreserves unrelatedt3.jsonfields, matches existing entries by command or case-insensitive name, enforces a 50-script limit, and returns pretty-printed JSON.setProjectFileQueryData. If writingt3.jsonfails, the action still saves locally and an error toast is shown.📊 Macroscope summarized d98df4f. 3 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted
🗂️ Filtered Issues
No issues evaluated.