feat(web): share project actions via t3.json - #4363

Closed
edoedac0 wants to merge 1 commit into
pingdotgg:mainfrom
edoedac0:feat/share-project-actions
Closed

feat(web): share project actions via t3.json#4363
edoedac0 wants to merge 1 commit into
pingdotgg:mainfrom
edoedac0:feat/share-project-actions

Conversation

@edoedac0

@edoedac0edoedac0 commented Jul 23, 2026

Copy link
Copy Markdown

What Changed

  • add an off-by-default “Share with everyone on this project” switch to the add/edit action dialog
  • create t3.json with the published schema when the first action is shared
  • merge new or edited actions into an existing t3.json without duplicating matching actions or dropping unrelated project settings
  • keep personal keybindings local and show a non-blocking error toast if the local action saves but t3.json cannot be updated
  • cover create, append, edit, invalid-file, forward-compatible-field, and script-limit behavior with focused tests

Why

t3.json already 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.json edit.

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 typecheck
  • pnpm 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.ts
  • pnpm exec vp test run apps/web/src/t3ProjectFileScripts.test.ts apps/web/src/components/files/projectFilesQueryState.test.ts (6 tests)
  • targeted formatting check for the five changed files
  • integrated web flow manually verified by the requester

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • No animation or motion behavior was changed, so a video is not applicable

Note

Medium Risk
Opt-in writes to version-controlled t3.json in 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 new getProjectFileQueryData), merges the action with upsertT3ProjectFileScript (create file, append, or update by name/command; keeps unrelated keys; 50-script cap; invalid JSON errors), then persists through writeProjectFile. 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

  • Adds a "Share with everyone on this project" toggle to the add/edit action dialog in ProjectScriptsControl, exposing a new shareWithProject flag on NewProjectScriptInput.
  • When shareWithProject is true, ChatViewContent writes or updates the action in the project's t3.json file after saving, using the new upsertT3ProjectFileScript utility in t3ProjectFileScripts.ts.
  • upsertT3ProjectFileScript preserves unrelated t3.json fields, matches existing entries by command or case-insensitive name, enforces a 50-script limit, and returns pretty-printed JSON.
  • On success, the in-memory query cache is updated via setProjectFileQueryData. If writing t3.json fails, 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.

@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a9c1321-6ff0-460d-b4ba-31bb71f339be

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 23, 2026
Comment on lines +76 to +83
const scripts = Array.isArray(rawProjectFile.scripts)
? [...rawProjectFile.scripts]
: [...decodedScripts];
if (existingIndex === -1) {
scripts.push(nextFileScript);
} else {
scripts[existingIndex] = nextFileScript;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumsrc/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.

Comment on lines +67 to +71
const existingIndex = decodedScripts.findIndex(
(fileScript) =>
(input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) ||
isSameScript(fileScript, input.script),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highsrc/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.

Suggested change
constexistingIndex=decodedScripts.findIndex(
(fileScript)=>
(input.previousScript!==undefined&&isSameScript(fileScript,input.previousScript))||
isSameScript(fileScript,input.script),
);
constmatchPrevious=(fileScript: T3ProjectFileScript)=>
input.previousScript!==undefined&&isSameScript(fileScript,input.previousScript);
constmatchCurrent=(fileScript: T3ProjectFileScript)=>
isSameScript(fileScript,input.script);
constpreviousIndex=decodedScripts.findIndex(matchPrevious);
constexistingIndex=
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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highfiles/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.

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ 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 } : {}),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit d98df4f. Configure here.

(fileScript) =>
(input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) ||
isSameScript(fileScript, input.script),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit d98df4f. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

The share action can write from an optional cached t3.json value without first reading the current server copy. That can replace unrelated project configuration, so this implementation is not safe to keep active.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotggt3dotgg closed this Aug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@edoedac0@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

feat(web): share project actions via t3.json - #4363

Closed
edoedac0 wants to merge 1 commit into
pingdotgg:mainfrom
edoedac0:feat/share-project-actions
Closed

feat(web): share project actions via t3.json#4363
edoedac0 wants to merge 1 commit into
pingdotgg:mainfrom
edoedac0:feat/share-project-actions

Conversation

@edoedac0

@edoedac0edoedac0 commented Jul 23, 2026

Copy link
Copy Markdown

What Changed

  • add an off-by-default “Share with everyone on this project” switch to the add/edit action dialog
  • create t3.json with the published schema when the first action is shared
  • merge new or edited actions into an existing t3.json without duplicating matching actions or dropping unrelated project settings
  • keep personal keybindings local and show a non-blocking error toast if the local action saves but t3.json cannot be updated
  • cover create, append, edit, invalid-file, forward-compatible-field, and script-limit behavior with focused tests

Why

t3.json already 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.json edit.

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 typecheck
  • pnpm 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.ts
  • pnpm exec vp test run apps/web/src/t3ProjectFileScripts.test.ts apps/web/src/components/files/projectFilesQueryState.test.ts (6 tests)
  • targeted formatting check for the five changed files
  • integrated web flow manually verified by the requester

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • No animation or motion behavior was changed, so a video is not applicable

Note

Medium Risk
Opt-in writes to version-controlled t3.json in 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 new getProjectFileQueryData), merges the action with upsertT3ProjectFileScript (create file, append, or update by name/command; keeps unrelated keys; 50-script cap; invalid JSON errors), then persists through writeProjectFile. 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

  • Adds a "Share with everyone on this project" toggle to the add/edit action dialog in ProjectScriptsControl, exposing a new shareWithProject flag on NewProjectScriptInput.
  • When shareWithProject is true, ChatViewContent writes or updates the action in the project's t3.json file after saving, using the new upsertT3ProjectFileScript utility in t3ProjectFileScripts.ts.
  • upsertT3ProjectFileScript preserves unrelated t3.json fields, matches existing entries by command or case-insensitive name, enforces a 50-script limit, and returns pretty-printed JSON.
  • On success, the in-memory query cache is updated via setProjectFileQueryData. If writing t3.json fails, 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.

@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a9c1321-6ff0-460d-b4ba-31bb71f339be

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 23, 2026
Comment on lines +76 to +83
const scripts = Array.isArray(rawProjectFile.scripts)
? [...rawProjectFile.scripts]
: [...decodedScripts];
if (existingIndex === -1) {
scripts.push(nextFileScript);
} else {
scripts[existingIndex] = nextFileScript;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumsrc/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.

Comment on lines +67 to +71
const existingIndex = decodedScripts.findIndex(
(fileScript) =>
(input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) ||
isSameScript(fileScript, input.script),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highsrc/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.

Suggested change
constexistingIndex=decodedScripts.findIndex(
(fileScript)=>
(input.previousScript!==undefined&&isSameScript(fileScript,input.previousScript))||
isSameScript(fileScript,input.script),
);
constmatchPrevious=(fileScript: T3ProjectFileScript)=>
input.previousScript!==undefined&&isSameScript(fileScript,input.previousScript);
constmatchCurrent=(fileScript: T3ProjectFileScript)=>
isSameScript(fileScript,input.script);
constpreviousIndex=decodedScripts.findIndex(matchPrevious);
constexistingIndex=
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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highfiles/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.

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ 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 } : {}),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit d98df4f. Configure here.

(fileScript) =>
(input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) ||
isSameScript(fileScript, input.script),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit d98df4f. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

The share action can write from an optional cached t3.json value without first reading the current server copy. That can replace unrelated project configuration, so this implementation is not safe to keep active.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotggt3dotgg closed this Aug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@edoedac0@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): share project actions via t3.json - #4363

Closed
edoedac0 wants to merge 1 commit into
pingdotgg:mainfrom
edoedac0:feat/share-project-actions
Closed

feat(web): share project actions via t3.json#4363
edoedac0 wants to merge 1 commit into
pingdotgg:mainfrom
edoedac0:feat/share-project-actions

Conversation

@edoedac0

@edoedac0edoedac0 commented Jul 23, 2026

Copy link
Copy Markdown

What Changed

  • add an off-by-default “Share with everyone on this project” switch to the add/edit action dialog
  • create t3.json with the published schema when the first action is shared
  • merge new or edited actions into an existing t3.json without duplicating matching actions or dropping unrelated project settings
  • keep personal keybindings local and show a non-blocking error toast if the local action saves but t3.json cannot be updated
  • cover create, append, edit, invalid-file, forward-compatible-field, and script-limit behavior with focused tests

Why

t3.json already 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.json edit.

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 typecheck
  • pnpm 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.ts
  • pnpm exec vp test run apps/web/src/t3ProjectFileScripts.test.ts apps/web/src/components/files/projectFilesQueryState.test.ts (6 tests)
  • targeted formatting check for the five changed files
  • integrated web flow manually verified by the requester

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • No animation or motion behavior was changed, so a video is not applicable

Note

Medium Risk
Opt-in writes to version-controlled t3.json in 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 new getProjectFileQueryData), merges the action with upsertT3ProjectFileScript (create file, append, or update by name/command; keeps unrelated keys; 50-script cap; invalid JSON errors), then persists through writeProjectFile. 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

  • Adds a "Share with everyone on this project" toggle to the add/edit action dialog in ProjectScriptsControl, exposing a new shareWithProject flag on NewProjectScriptInput.
  • When shareWithProject is true, ChatViewContent writes or updates the action in the project's t3.json file after saving, using the new upsertT3ProjectFileScript utility in t3ProjectFileScripts.ts.
  • upsertT3ProjectFileScript preserves unrelated t3.json fields, matches existing entries by command or case-insensitive name, enforces a 50-script limit, and returns pretty-printed JSON.
  • On success, the in-memory query cache is updated via setProjectFileQueryData. If writing t3.json fails, 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.

@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a9c1321-6ff0-460d-b4ba-31bb71f339be

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 23, 2026
Comment on lines +76 to +83
const scripts = Array.isArray(rawProjectFile.scripts)
? [...rawProjectFile.scripts]
: [...decodedScripts];
if (existingIndex === -1) {
scripts.push(nextFileScript);
} else {
scripts[existingIndex] = nextFileScript;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumsrc/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.

Comment on lines +67 to +71
const existingIndex = decodedScripts.findIndex(
(fileScript) =>
(input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) ||
isSameScript(fileScript, input.script),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highsrc/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.

Suggested change
constexistingIndex=decodedScripts.findIndex(
(fileScript)=>
(input.previousScript!==undefined&&isSameScript(fileScript,input.previousScript))||
isSameScript(fileScript,input.script),
);
constmatchPrevious=(fileScript: T3ProjectFileScript)=>
input.previousScript!==undefined&&isSameScript(fileScript,input.previousScript);
constmatchCurrent=(fileScript: T3ProjectFileScript)=>
isSameScript(fileScript,input.script);
constpreviousIndex=decodedScripts.findIndex(matchPrevious);
constexistingIndex=
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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highfiles/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.

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ 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 } : {}),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit d98df4f. Configure here.

(fileScript) =>
(input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) ||
isSameScript(fileScript, input.script),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit d98df4f. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

The share action can write from an optional cached t3.json value without first reading the current server copy. That can replace unrelated project configuration, so this implementation is not safe to keep active.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotggt3dotgg closed this Aug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@edoedac0@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): share project actions via t3.json - #4363

Closed
edoedac0 wants to merge 1 commit into
pingdotgg:mainfrom
edoedac0:feat/share-project-actions
Closed

feat(web): share project actions via t3.json#4363
edoedac0 wants to merge 1 commit into
pingdotgg:mainfrom
edoedac0:feat/share-project-actions

Conversation

@edoedac0

@edoedac0edoedac0 commented Jul 23, 2026

Copy link
Copy Markdown

What Changed

  • add an off-by-default “Share with everyone on this project” switch to the add/edit action dialog
  • create t3.json with the published schema when the first action is shared
  • merge new or edited actions into an existing t3.json without duplicating matching actions or dropping unrelated project settings
  • keep personal keybindings local and show a non-blocking error toast if the local action saves but t3.json cannot be updated
  • cover create, append, edit, invalid-file, forward-compatible-field, and script-limit behavior with focused tests

Why

t3.json already 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.json edit.

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 typecheck
  • pnpm 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.ts
  • pnpm exec vp test run apps/web/src/t3ProjectFileScripts.test.ts apps/web/src/components/files/projectFilesQueryState.test.ts (6 tests)
  • targeted formatting check for the five changed files
  • integrated web flow manually verified by the requester

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • No animation or motion behavior was changed, so a video is not applicable

Note

Medium Risk
Opt-in writes to version-controlled t3.json in 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 new getProjectFileQueryData), merges the action with upsertT3ProjectFileScript (create file, append, or update by name/command; keeps unrelated keys; 50-script cap; invalid JSON errors), then persists through writeProjectFile. 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

  • Adds a "Share with everyone on this project" toggle to the add/edit action dialog in ProjectScriptsControl, exposing a new shareWithProject flag on NewProjectScriptInput.
  • When shareWithProject is true, ChatViewContent writes or updates the action in the project's t3.json file after saving, using the new upsertT3ProjectFileScript utility in t3ProjectFileScripts.ts.
  • upsertT3ProjectFileScript preserves unrelated t3.json fields, matches existing entries by command or case-insensitive name, enforces a 50-script limit, and returns pretty-printed JSON.
  • On success, the in-memory query cache is updated via setProjectFileQueryData. If writing t3.json fails, 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.

@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a9c1321-6ff0-460d-b4ba-31bb71f339be

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 23, 2026
Comment on lines +76 to +83
const scripts = Array.isArray(rawProjectFile.scripts)
? [...rawProjectFile.scripts]
: [...decodedScripts];
if (existingIndex === -1) {
scripts.push(nextFileScript);
} else {
scripts[existingIndex] = nextFileScript;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumsrc/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.

Comment on lines +67 to +71
const existingIndex = decodedScripts.findIndex(
(fileScript) =>
(input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) ||
isSameScript(fileScript, input.script),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highsrc/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.

Suggested change
constexistingIndex=decodedScripts.findIndex(
(fileScript)=>
(input.previousScript!==undefined&&isSameScript(fileScript,input.previousScript))||
isSameScript(fileScript,input.script),
);
constmatchPrevious=(fileScript: T3ProjectFileScript)=>
input.previousScript!==undefined&&isSameScript(fileScript,input.previousScript);
constmatchCurrent=(fileScript: T3ProjectFileScript)=>
isSameScript(fileScript,input.script);
constpreviousIndex=decodedScripts.findIndex(matchPrevious);
constexistingIndex=
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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highfiles/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.

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ 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 } : {}),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit d98df4f. Configure here.

(fileScript) =>
(input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) ||
isSameScript(fileScript, input.script),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit d98df4f. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

The share action can write from an optional cached t3.json value without first reading the current server copy. That can replace unrelated project configuration, so this implementation is not safe to keep active.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotggt3dotgg closed this Aug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@edoedac0@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

feat(web): share project actions via t3.json - #4363

Closed
edoedac0 wants to merge 1 commit into
pingdotgg:mainfrom
edoedac0:feat/share-project-actions
Closed

feat(web): share project actions via t3.json#4363
edoedac0 wants to merge 1 commit into
pingdotgg:mainfrom
edoedac0:feat/share-project-actions

Conversation

@edoedac0

@edoedac0edoedac0 commented Jul 23, 2026

Copy link
Copy Markdown

What Changed

  • add an off-by-default “Share with everyone on this project” switch to the add/edit action dialog
  • create t3.json with the published schema when the first action is shared
  • merge new or edited actions into an existing t3.json without duplicating matching actions or dropping unrelated project settings
  • keep personal keybindings local and show a non-blocking error toast if the local action saves but t3.json cannot be updated
  • cover create, append, edit, invalid-file, forward-compatible-field, and script-limit behavior with focused tests

Why

t3.json already 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.json edit.

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 typecheck
  • pnpm 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.ts
  • pnpm exec vp test run apps/web/src/t3ProjectFileScripts.test.ts apps/web/src/components/files/projectFilesQueryState.test.ts (6 tests)
  • targeted formatting check for the five changed files
  • integrated web flow manually verified by the requester

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • No animation or motion behavior was changed, so a video is not applicable

Note

Medium Risk
Opt-in writes to version-controlled t3.json in 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 new getProjectFileQueryData), merges the action with upsertT3ProjectFileScript (create file, append, or update by name/command; keeps unrelated keys; 50-script cap; invalid JSON errors), then persists through writeProjectFile. 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

  • Adds a "Share with everyone on this project" toggle to the add/edit action dialog in ProjectScriptsControl, exposing a new shareWithProject flag on NewProjectScriptInput.
  • When shareWithProject is true, ChatViewContent writes or updates the action in the project's t3.json file after saving, using the new upsertT3ProjectFileScript utility in t3ProjectFileScripts.ts.
  • upsertT3ProjectFileScript preserves unrelated t3.json fields, matches existing entries by command or case-insensitive name, enforces a 50-script limit, and returns pretty-printed JSON.
  • On success, the in-memory query cache is updated via setProjectFileQueryData. If writing t3.json fails, 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.

@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a9c1321-6ff0-460d-b4ba-31bb71f339be

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 23, 2026
Comment on lines +76 to +83
const scripts = Array.isArray(rawProjectFile.scripts)
? [...rawProjectFile.scripts]
: [...decodedScripts];
if (existingIndex === -1) {
scripts.push(nextFileScript);
} else {
scripts[existingIndex] = nextFileScript;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumsrc/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.

Comment on lines +67 to +71
const existingIndex = decodedScripts.findIndex(
(fileScript) =>
(input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) ||
isSameScript(fileScript, input.script),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highsrc/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.

Suggested change
constexistingIndex=decodedScripts.findIndex(
(fileScript)=>
(input.previousScript!==undefined&&isSameScript(fileScript,input.previousScript))||
isSameScript(fileScript,input.script),
);
constmatchPrevious=(fileScript: T3ProjectFileScript)=>
input.previousScript!==undefined&&isSameScript(fileScript,input.previousScript);
constmatchCurrent=(fileScript: T3ProjectFileScript)=>
isSameScript(fileScript,input.script);
constpreviousIndex=decodedScripts.findIndex(matchPrevious);
constexistingIndex=
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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highfiles/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.

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ 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 } : {}),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit d98df4f. Configure here.

(fileScript) =>
(input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) ||
isSameScript(fileScript, input.script),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit d98df4f. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

The share action can write from an optional cached t3.json value without first reading the current server copy. That can replace unrelated project configuration, so this implementation is not safe to keep active.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotggt3dotgg closed this Aug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@edoedac0@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): share project actions via t3.json - #4363

Closed
edoedac0 wants to merge 1 commit into
pingdotgg:mainfrom
edoedac0:feat/share-project-actions
Closed

feat(web): share project actions via t3.json#4363
edoedac0 wants to merge 1 commit into
pingdotgg:mainfrom
edoedac0:feat/share-project-actions

Conversation

@edoedac0

@edoedac0edoedac0 commented Jul 23, 2026

Copy link
Copy Markdown

What Changed

  • add an off-by-default “Share with everyone on this project” switch to the add/edit action dialog
  • create t3.json with the published schema when the first action is shared
  • merge new or edited actions into an existing t3.json without duplicating matching actions or dropping unrelated project settings
  • keep personal keybindings local and show a non-blocking error toast if the local action saves but t3.json cannot be updated
  • cover create, append, edit, invalid-file, forward-compatible-field, and script-limit behavior with focused tests

Why

t3.json already 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.json edit.

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 typecheck
  • pnpm 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.ts
  • pnpm exec vp test run apps/web/src/t3ProjectFileScripts.test.ts apps/web/src/components/files/projectFilesQueryState.test.ts (6 tests)
  • targeted formatting check for the five changed files
  • integrated web flow manually verified by the requester

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • No animation or motion behavior was changed, so a video is not applicable

Note

Medium Risk
Opt-in writes to version-controlled t3.json in 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 new getProjectFileQueryData), merges the action with upsertT3ProjectFileScript (create file, append, or update by name/command; keeps unrelated keys; 50-script cap; invalid JSON errors), then persists through writeProjectFile. 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

  • Adds a "Share with everyone on this project" toggle to the add/edit action dialog in ProjectScriptsControl, exposing a new shareWithProject flag on NewProjectScriptInput.
  • When shareWithProject is true, ChatViewContent writes or updates the action in the project's t3.json file after saving, using the new upsertT3ProjectFileScript utility in t3ProjectFileScripts.ts.
  • upsertT3ProjectFileScript preserves unrelated t3.json fields, matches existing entries by command or case-insensitive name, enforces a 50-script limit, and returns pretty-printed JSON.
  • On success, the in-memory query cache is updated via setProjectFileQueryData. If writing t3.json fails, 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.

@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a9c1321-6ff0-460d-b4ba-31bb71f339be

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 23, 2026
Comment on lines +76 to +83
const scripts = Array.isArray(rawProjectFile.scripts)
? [...rawProjectFile.scripts]
: [...decodedScripts];
if (existingIndex === -1) {
scripts.push(nextFileScript);
} else {
scripts[existingIndex] = nextFileScript;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumsrc/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.

Comment on lines +67 to +71
const existingIndex = decodedScripts.findIndex(
(fileScript) =>
(input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) ||
isSameScript(fileScript, input.script),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highsrc/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.

Suggested change
constexistingIndex=decodedScripts.findIndex(
(fileScript)=>
(input.previousScript!==undefined&&isSameScript(fileScript,input.previousScript))||
isSameScript(fileScript,input.script),
);
constmatchPrevious=(fileScript: T3ProjectFileScript)=>
input.previousScript!==undefined&&isSameScript(fileScript,input.previousScript);
constmatchCurrent=(fileScript: T3ProjectFileScript)=>
isSameScript(fileScript,input.script);
constpreviousIndex=decodedScripts.findIndex(matchPrevious);
constexistingIndex=
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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highfiles/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.

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ 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 } : {}),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit d98df4f. Configure here.

(fileScript) =>
(input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) ||
isSameScript(fileScript, input.script),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit d98df4f. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

The share action can write from an optional cached t3.json value without first reading the current server copy. That can replace unrelated project configuration, so this implementation is not safe to keep active.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotggt3dotgg closed this Aug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@edoedac0@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

feat(web): share project actions via t3.json - #4363

Closed
edoedac0 wants to merge 1 commit into
pingdotgg:mainfrom
edoedac0:feat/share-project-actions
Closed

feat(web): share project actions via t3.json#4363
edoedac0 wants to merge 1 commit into
pingdotgg:mainfrom
edoedac0:feat/share-project-actions

Conversation

@edoedac0

@edoedac0edoedac0 commented Jul 23, 2026

Copy link
Copy Markdown

What Changed

  • add an off-by-default “Share with everyone on this project” switch to the add/edit action dialog
  • create t3.json with the published schema when the first action is shared
  • merge new or edited actions into an existing t3.json without duplicating matching actions or dropping unrelated project settings
  • keep personal keybindings local and show a non-blocking error toast if the local action saves but t3.json cannot be updated
  • cover create, append, edit, invalid-file, forward-compatible-field, and script-limit behavior with focused tests

Why

t3.json already 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.json edit.

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 typecheck
  • pnpm 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.ts
  • pnpm exec vp test run apps/web/src/t3ProjectFileScripts.test.ts apps/web/src/components/files/projectFilesQueryState.test.ts (6 tests)
  • targeted formatting check for the five changed files
  • integrated web flow manually verified by the requester

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • No animation or motion behavior was changed, so a video is not applicable

Note

Medium Risk
Opt-in writes to version-controlled t3.json in 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 new getProjectFileQueryData), merges the action with upsertT3ProjectFileScript (create file, append, or update by name/command; keeps unrelated keys; 50-script cap; invalid JSON errors), then persists through writeProjectFile. 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

  • Adds a "Share with everyone on this project" toggle to the add/edit action dialog in ProjectScriptsControl, exposing a new shareWithProject flag on NewProjectScriptInput.
  • When shareWithProject is true, ChatViewContent writes or updates the action in the project's t3.json file after saving, using the new upsertT3ProjectFileScript utility in t3ProjectFileScripts.ts.
  • upsertT3ProjectFileScript preserves unrelated t3.json fields, matches existing entries by command or case-insensitive name, enforces a 50-script limit, and returns pretty-printed JSON.
  • On success, the in-memory query cache is updated via setProjectFileQueryData. If writing t3.json fails, 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.

@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a9c1321-6ff0-460d-b4ba-31bb71f339be

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 23, 2026
Comment on lines +76 to +83
const scripts = Array.isArray(rawProjectFile.scripts)
? [...rawProjectFile.scripts]
: [...decodedScripts];
if (existingIndex === -1) {
scripts.push(nextFileScript);
} else {
scripts[existingIndex] = nextFileScript;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumsrc/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.

Comment on lines +67 to +71
const existingIndex = decodedScripts.findIndex(
(fileScript) =>
(input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) ||
isSameScript(fileScript, input.script),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highsrc/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.

Suggested change
constexistingIndex=decodedScripts.findIndex(
(fileScript)=>
(input.previousScript!==undefined&&isSameScript(fileScript,input.previousScript))||
isSameScript(fileScript,input.script),
);
constmatchPrevious=(fileScript: T3ProjectFileScript)=>
input.previousScript!==undefined&&isSameScript(fileScript,input.previousScript);
constmatchCurrent=(fileScript: T3ProjectFileScript)=>
isSameScript(fileScript,input.script);
constpreviousIndex=decodedScripts.findIndex(matchPrevious);
constexistingIndex=
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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highfiles/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.

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ 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 } : {}),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit d98df4f. Configure here.

(fileScript) =>
(input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) ||
isSameScript(fileScript, input.script),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit d98df4f. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

The share action can write from an optional cached t3.json value without first reading the current server copy. That can replace unrelated project configuration, so this implementation is not safe to keep active.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotggt3dotgg closed this Aug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@edoedac0@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

feat(web): share project actions via t3.json - #4363

Closed
edoedac0 wants to merge 1 commit into
pingdotgg:mainfrom
edoedac0:feat/share-project-actions
Closed

feat(web): share project actions via t3.json#4363
edoedac0 wants to merge 1 commit into
pingdotgg:mainfrom
edoedac0:feat/share-project-actions

Conversation

@edoedac0

@edoedac0edoedac0 commented Jul 23, 2026

Copy link
Copy Markdown

What Changed

  • add an off-by-default “Share with everyone on this project” switch to the add/edit action dialog
  • create t3.json with the published schema when the first action is shared
  • merge new or edited actions into an existing t3.json without duplicating matching actions or dropping unrelated project settings
  • keep personal keybindings local and show a non-blocking error toast if the local action saves but t3.json cannot be updated
  • cover create, append, edit, invalid-file, forward-compatible-field, and script-limit behavior with focused tests

Why

t3.json already 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.json edit.

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 typecheck
  • pnpm 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.ts
  • pnpm exec vp test run apps/web/src/t3ProjectFileScripts.test.ts apps/web/src/components/files/projectFilesQueryState.test.ts (6 tests)
  • targeted formatting check for the five changed files
  • integrated web flow manually verified by the requester

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • No animation or motion behavior was changed, so a video is not applicable

Note

Medium Risk
Opt-in writes to version-controlled t3.json in 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 new getProjectFileQueryData), merges the action with upsertT3ProjectFileScript (create file, append, or update by name/command; keeps unrelated keys; 50-script cap; invalid JSON errors), then persists through writeProjectFile. 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

  • Adds a "Share with everyone on this project" toggle to the add/edit action dialog in ProjectScriptsControl, exposing a new shareWithProject flag on NewProjectScriptInput.
  • When shareWithProject is true, ChatViewContent writes or updates the action in the project's t3.json file after saving, using the new upsertT3ProjectFileScript utility in t3ProjectFileScripts.ts.
  • upsertT3ProjectFileScript preserves unrelated t3.json fields, matches existing entries by command or case-insensitive name, enforces a 50-script limit, and returns pretty-printed JSON.
  • On success, the in-memory query cache is updated via setProjectFileQueryData. If writing t3.json fails, 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.

@coderabbitai

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 1a9c1321-6ff0-460d-b4ba-31bb71f339be

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actionsgithub-actionsBot added size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 23, 2026
Comment on lines +76 to +83
const scripts = Array.isArray(rawProjectFile.scripts)
? [...rawProjectFile.scripts]
: [...decodedScripts];
if (existingIndex === -1) {
scripts.push(nextFileScript);
} else {
scripts[existingIndex] = nextFileScript;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Mediumsrc/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.

Comment on lines +67 to +71
const existingIndex = decodedScripts.findIndex(
(fileScript) =>
(input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) ||
isSameScript(fileScript, input.script),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highsrc/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.

Suggested change
constexistingIndex=decodedScripts.findIndex(
(fileScript)=>
(input.previousScript!==undefined&&isSameScript(fileScript,input.previousScript))||
isSameScript(fileScript,input.script),
);
constmatchPrevious=(fileScript: T3ProjectFileScript)=>
input.previousScript!==undefined&&isSameScript(fileScript,input.previousScript);
constmatchCurrent=(fileScript: T3ProjectFileScript)=>
isSameScript(fileScript,input.script);
constpreviousIndex=decodedScripts.findIndex(matchPrevious);
constexistingIndex=
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(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 Highfiles/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.

@cursorcursorBot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ 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 } : {}),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit d98df4f. Configure here.

(fileScript) =>
(input.previousScript !== undefined && isSameScript(fileScript, input.previousScript)) ||
isSameScript(fileScript, input.script),
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit d98df4f. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: 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.

@t3dotgg

Copy link
Copy Markdown
Member

Note

🤖 GPT-5.6 Sol responding on behalf of Theo

We're closing this PR as we clean up the T3 Code backlog. Thank you for taking the time to put this together.

The share action can write from an optional cached t3.json value without first reading the current server copy. That can replace unrelated project configuration, so this implementation is not safe to keep active.

If you believe we closed this in error, please reopen the PR and leave a comment explaining what we missed.

@t3dotggt3dotgg closed this Aug 28, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L100-499 changed lines (additions + deletions).vouch:unvouchedPR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@edoedac0@t3dotgg