feat!(agent/slop): ability to copy prompt and open in agent from task#2815
Conversation
📝 WalkthroughSummary by CodeRabbit
WalkthroughIntroduces a new Changes
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@js/app/packages/block-md/component/DispatchAgentMenu.tsx`:
- Around line 196-210: The primary button provides no in-flight feedback and
executeAction can be re-entered; add a loading state (e.g., isDispatching) to
DispatchAgentMenu, set it true before calling buildPrompt() in executeAction and
false in finally, and update handlePrimaryClick to no-op while isDispatching to
guard re-entry; also pass isDispatching to the primary button so it is disabled
and shows a spinner/icon while awaiting buildPrompt()/action.execute(), and
ensure setOpen(false) still runs after completion or error in the finally block
of executeAction.
- Around line 89-92: The current code builds userId as comment.sender ??
comment.owner and passes it to tryMacroId and then sets author to macroId ?
macroIdToEmail(macroId) : userId, which can yield "undefined"; change the guard
so userId is never undefined (e.g. const userId = comment.sender ??
comment.owner ?? 'unknown') or skip the comment when both are missing, and
ensure you only call tryMacroId when userId is defined/non-placeholder; update
the author assignment (author = macroId ? macroIdToEmail(macroId) : userId)
accordingly to avoid author === "undefined".
- Around line 87-98: The code currently resolves macroId to a real email via
macroIdToEmail(macroId) when building the author string (in the loop that uses
tryMacroId, macroIdToEmail, author, and pushes `<comment author="...">`), which
leaks PII; change this to use a non-PII identifier (e.g., display name, handle,
or a stable opaque id) instead of email by replacing the macroIdToEmail call
with a safe resolver (e.g., macroIdToDisplayName or macroIdToOpaqueId) or
fallback to the original userId, and optionally gate the behavior behind a
workspace/feature setting so emails are never emitted to external
agents/clipboard by DispatchAgentMenu.tsx. Ensure the new resolver returns only
non-email values and update any tests or callers that expect an email.
- Around line 69-103: The code in DispatchAgentMenu.tsx interpolates
user-controlled strings (documentName, content, author, comment.text) directly
into XML-like tags and attributes; add a small escape helper (e.g.,
escapeXmlForAttr and escapeXmlForText or a single paramized escapeXml) and use
it when building the lines: escape attribute contexts (author, thread-id if
needed, any attribute values like shortId/branchName) replacing &, <, >, " and
escape element text contexts (title, task-content, comment.text) replacing &, <,
>; update the pushes that create `<title>${...}</title>`, `<task-content>`,
`<comment author="${author}"...>...</comment>` and any attribute interpolations
so they call the appropriate escape helper (keeping tryMacroId and
macroIdToEmail usage intact).
- Around line 139-167: Update each agent's execute handler in
DispatchAgentMenu.tsx (the entries with keys 'claude', 'codex-desktop',
'cursor', and 'zed') to call window.open with a features string including
"noopener,noreferrer" as the third argument (i.e., window.open(url, '_blank',
'noopener,noreferrer')) so the opened cross-origin pages do not receive the
opener reference or referrer; apply this change to all four execute functions.
- Around line 196-206: The user-activation is lost across the await in
executeAction causing window.open in PLATFORM_ACTIONS to be blocked; fix by
detecting platform/open-in-IDE actions inside executeAction, synchronously call
const win = window.open('', '_blank') before await buildPrompt(), then await
buildPrompt(), pass the prompt (and the win reference if needed) to
action.execute(prompt, win) or set win.location.href to the final URL returned
by the action; if win is null (blocked) or the action fails to produce a URL,
show a toast.failure and avoid silent failure; keep setLastUsedKey(action.key)
and setOpen(false) behavior but ensure window.open happens before the async
boundary and that action.execute can accept or return the URL to assign to win.
- Around line 42-54: Extract the duplicated branch-name logic into a shared
utility and replace the local buildBranchName in DispatchAgentMenu with a call
to it: move the logic that calls storageServiceClient.getDocumentShortId, uses
slugify, composes `macro-${shortId}${slug ? `-${slug}` : ''}` and slices to
MAX_BRANCH_LENGTH into the existing shared module (e.g.,
js/app/packages/core/util/branchName.ts) as a exported function (e.g.,
buildBranchName), preserve the current shortId fallback and error handling
semantics, then import and use that shared buildBranchName in DispatchAgentMenu
while removing the local function to ensure both callers use the same behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 1c1ecf96-e8a0-4082-a684-c955e7a30d9c
⛔ Files ignored due to path filters (4)
js/app/packages/macro-icons/wide/claude.svgis excluded by!**/*.svgjs/app/packages/macro-icons/wide/codex-ide.svgis excluded by!**/*.svgjs/app/packages/macro-icons/wide/cursor-ide.svgis excluded by!**/*.svgjs/app/packages/macro-icons/wide/zed-ide.svgis excluded by!**/*.svg
📒 Files selected for processing (2)
js/app/packages/block-md/component/DispatchAgentMenu.tsxjs/app/packages/block-md/component/TopBar.tsx
| async function buildBranchName( | ||
| documentId: string, | ||
| documentName: string | ||
| ): Promise<{ shortId: string; branchName: string }> { | ||
| const result = await storageServiceClient.getDocumentShortId({ documentId }); | ||
| const shortId = isOk(result) ? result[1] : documentId; | ||
| const slug = slugify(documentName); | ||
| const branchName = `macro-${shortId}${slug ? `-${slug}` : ''}`.slice( | ||
| 0, | ||
| MAX_BRANCH_LENGTH | ||
| ); | ||
| return { shortId, branchName }; | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Look for an existing short-id query in the queries package to reuse.
fd -t f . | rg -n 'queries' | head
rg -nP --type=ts -C2 'getDocumentShortId\b'Repository: macro-inc/macro
Length of output: 1833
🏁 Script executed:
# Check if there's a TanStack Query wrapper for documentShortId
rg -n 'documentShortId\|getDocumentShortId' js/app/packages/queries --type=ts
# Check the full structure and context of DispatchAgentMenu.tsx
head -70 js/app/packages/block-md/component/DispatchAgentMenu.tsx
# Check if other block-* packages follow the same pattern
rg -l 'storageServiceClient\.' js/app/packages/block-*/Repository: macro-inc/macro
Length of output: 3812
🏁 Script executed:
# Compare the two buildBranchName implementations
echo "=== DispatchAgentMenu.tsx version ===" && \
sed -n '42,54p' js/app/packages/block-md/component/DispatchAgentMenu.tsx && \
echo -e "\n=== core/util/branchName.ts version ===" && \
sed -n '16,28p' js/app/packages/core/util/branchName.ts
# Check if block-md calls service clients elsewhere (confirming it's an accepted pattern)
rg -l 'storageServiceClient\.' js/app/packages/block-md/Repository: macro-inc/macro
Length of output: 1237
Consider extracting buildBranchName to a shared utility to avoid duplication.
The logic in this function duplicates logic already present in js/app/packages/core/util/branchName.ts. Extract a reusable buildBranchName function to a shared location so both call sites can use it, ensuring consistent error handling and reducing maintenance burden.
Note: Direct calls to storageServiceClient are an accepted pattern in non-queries packages like block-md.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@js/app/packages/block-md/component/DispatchAgentMenu.tsx` around lines 42 -
54, Extract the duplicated branch-name logic into a shared utility and replace
the local buildBranchName in DispatchAgentMenu with a call to it: move the logic
that calls storageServiceClient.getDocumentShortId, uses slugify, composes
`macro-${shortId}${slug ? `-${slug}` : ''}` and slices to MAX_BRANCH_LENGTH into
the existing shared module (e.g., js/app/packages/core/util/branchName.ts) as a
exported function (e.g., buildBranchName), preserve the current shortId fallback
and error handling semantics, then import and use that shared buildBranchName in
DispatchAgentMenu while removing the local function to ensure both callers use
the same behavior.
| lines.push(`Work on Macro task ${documentName}:`); | ||
| lines.push(''); | ||
| lines.push(`<task identifier="${shortId}">`); | ||
| lines.push(`<title>${documentName}</title>`); | ||
| lines.push(`<branch>${branchName}</branch>`); | ||
| lines.push('</task>'); | ||
|
|
||
| if (content) { | ||
| lines.push(''); | ||
| lines.push('<task-content>'); | ||
| lines.push(content); | ||
| lines.push('</task-content>'); | ||
| } | ||
|
|
||
| if (threads.length > 0) { | ||
| lines.push(''); | ||
| for (const thread of threads) { | ||
| const sorted = [...thread.comments].sort(sortComments); | ||
| lines.push(`<comment-thread thread-id="${thread.thread.threadId}">`); | ||
| for (const comment of sorted) { | ||
| if (comment.text && !comment.deletedAt) { | ||
| const userId = comment.sender ?? comment.owner; | ||
| const macroId = tryMacroId(userId); | ||
| const author = macroId ? macroIdToEmail(macroId) : userId; | ||
| const createdAt = comment.createdAt | ||
| ? ` created-at="${comment.createdAt}"` | ||
| : ''; | ||
| lines.push( | ||
| `<comment author="${author}"${createdAt}>${comment.text}</comment>` | ||
| ); | ||
| } | ||
| } | ||
| lines.push('</comment-thread>'); | ||
| } | ||
| } |
There was a problem hiding this comment.
User-controlled strings are interpolated into XML-like tags without escaping.
documentName, content, author, and comment.text are inserted verbatim into <title>, <task-content>, attribute values, and <comment> bodies. A task title containing ", <, or a literal </comment> / </task-content> in a comment or the body will corrupt the structure the agent sees. Not a security issue (the consumer is an LLM / clipboard, not an XML parser), but it degrades prompt quality and can trivially confuse agents via crafted comment text.
Minimally, escape &, <, >, " for attribute values and &, <, > for element text before interpolation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@js/app/packages/block-md/component/DispatchAgentMenu.tsx` around lines 69 -
103, The code in DispatchAgentMenu.tsx interpolates user-controlled strings
(documentName, content, author, comment.text) directly into XML-like tags and
attributes; add a small escape helper (e.g., escapeXmlForAttr and
escapeXmlForText or a single paramized escapeXml) and use it when building the
lines: escape attribute contexts (author, thread-id if needed, any attribute
values like shortId/branchName) replacing &, <, >, " and escape element text
contexts (title, task-content, comment.text) replacing &, <, >; update the
pushes that create `<title>${...}</title>`, `<task-content>`, `<comment
author="${author}"...>...</comment>` and any attribute interpolations so they
call the appropriate escape helper (keeping tryMacroId and macroIdToEmail usage
intact).
| execute: (prompt) => | ||
| window.open( | ||
| `https://claude.ai/code?q=${encodeURIComponent(prompt)}`, | ||
| '_blank' | ||
| ), | ||
| }, | ||
| { | ||
| key: 'codex-desktop', | ||
| name: 'Codex Desktop', | ||
| icon: CodexIcon, | ||
| execute: (prompt) => | ||
| window.open(`codex://new?prompt=${encodeURIComponent(prompt)}`, '_blank'), | ||
| }, | ||
| { | ||
| key: 'cursor', | ||
| name: 'Cursor', | ||
| icon: CursorIcon, | ||
| execute: (prompt) => | ||
| window.open( | ||
| `cursor://anysphere.cursor-deeplink/prompt?text=${encodeURIComponent(prompt)}`, | ||
| '_blank' | ||
| ), | ||
| }, | ||
| { | ||
| key: 'zed', | ||
| name: 'Zed', | ||
| icon: ZedIcon, | ||
| execute: (prompt) => | ||
| window.open(`zed://agent?prompt=${encodeURIComponent(prompt)}`, '_blank'), |
There was a problem hiding this comment.
Pass noopener,noreferrer to window.open.
The claude.ai URL opens a cross-origin document with the default window.open features, which leaks the opener reference and the Referer header. Add the features string to all four handlers (especially relevant for the https://claude.ai/... target; harmless for custom schemes).
- window.open(`...`, '_blank'),
+ window.open(`...`, '_blank', 'noopener,noreferrer'),📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| execute: (prompt) => | |
| window.open( | |
| `https://claude.ai/code?q=${encodeURIComponent(prompt)}`, | |
| '_blank' | |
| ), | |
| }, | |
| { | |
| key: 'codex-desktop', | |
| name: 'Codex Desktop', | |
| icon: CodexIcon, | |
| execute: (prompt) => | |
| window.open(`codex://new?prompt=${encodeURIComponent(prompt)}`, '_blank'), | |
| }, | |
| { | |
| key: 'cursor', | |
| name: 'Cursor', | |
| icon: CursorIcon, | |
| execute: (prompt) => | |
| window.open( | |
| `cursor://anysphere.cursor-deeplink/prompt?text=${encodeURIComponent(prompt)}`, | |
| '_blank' | |
| ), | |
| }, | |
| { | |
| key: 'zed', | |
| name: 'Zed', | |
| icon: ZedIcon, | |
| execute: (prompt) => | |
| window.open(`zed://agent?prompt=${encodeURIComponent(prompt)}`, '_blank'), | |
| execute: (prompt) => | |
| window.open( | |
| `https://claude.ai/code?q=${encodeURIComponent(prompt)}`, | |
| '_blank', | |
| 'noopener,noreferrer' | |
| ), | |
| }, | |
| { | |
| key: 'codex-desktop', | |
| name: 'Codex Desktop', | |
| icon: CodexIcon, | |
| execute: (prompt) => | |
| window.open(`codex://new?prompt=${encodeURIComponent(prompt)}`, '_blank', 'noopener,noreferrer'), | |
| }, | |
| { | |
| key: 'cursor', | |
| name: 'Cursor', | |
| icon: CursorIcon, | |
| execute: (prompt) => | |
| window.open( | |
| `cursor://anysphere.cursor-deeplink/prompt?text=${encodeURIComponent(prompt)}`, | |
| '_blank', | |
| 'noopener,noreferrer' | |
| ), | |
| }, | |
| { | |
| key: 'zed', | |
| name: 'Zed', | |
| icon: ZedIcon, | |
| execute: (prompt) => | |
| window.open(`zed://agent?prompt=${encodeURIComponent(prompt)}`, '_blank', 'noopener,noreferrer'), |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@js/app/packages/block-md/component/DispatchAgentMenu.tsx` around lines 139 -
167, Update each agent's execute handler in DispatchAgentMenu.tsx (the entries
with keys 'claude', 'codex-desktop', 'cursor', and 'zed') to call window.open
with a features string including "noopener,noreferrer" as the third argument
(i.e., window.open(url, '_blank', 'noopener,noreferrer')) so the opened
cross-origin pages do not receive the opener reference or referrer; apply this
change to all four execute functions.
| const executeAction = async (action: AgentAction) => { | ||
| try { | ||
| const prompt = await buildPrompt(); | ||
| action.execute(prompt); | ||
| setLastUsedKey(action.key); | ||
| } catch (e) { | ||
| console.error('Failed to generate task prompt', e); | ||
| toast.failure('Failed to generate task prompt'); | ||
| } | ||
| setOpen(false); | ||
| }; | ||
|
|
||
| const handlePrimaryClick = () => { | ||
| executeAction(lastUsed()); | ||
| }; |
There was a problem hiding this comment.
No in-flight feedback for the primary button.
buildPrompt() hits the network (short-id lookup). Between click and completion the button gives no visual indication, users can re-click, and on slow networks the clipboard toast / window.open happens seconds later with no context. Consider disabling the button / swapping to a spinner icon while a dispatch is in flight, and guarding executeAction against re-entry.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@js/app/packages/block-md/component/DispatchAgentMenu.tsx` around lines 196 -
210, The primary button provides no in-flight feedback and executeAction can be
re-entered; add a loading state (e.g., isDispatching) to DispatchAgentMenu, set
it true before calling buildPrompt() in executeAction and false in finally, and
update handlePrimaryClick to no-op while isDispatching to guard re-entry; also
pass isDispatching to the primary button so it is disabled and shows a
spinner/icon while awaiting buildPrompt()/action.execute(), and ensure
setOpen(false) still runs after completion or error in the finally block of
executeAction.
| const executeAction = async (action: AgentAction) => { | ||
| try { | ||
| const prompt = await buildPrompt(); | ||
| action.execute(prompt); | ||
| setLastUsedKey(action.key); | ||
| } catch (e) { | ||
| console.error('Failed to generate task prompt', e); | ||
| toast.failure('Failed to generate task prompt'); | ||
| } | ||
| setOpen(false); | ||
| }; |
There was a problem hiding this comment.
Popup blocker will kill the "Open in " flow.
executeAction awaits buildPrompt() (which makes a network call via storageServiceClient.getDocumentShortId) before calling action.execute(prompt). For the PLATFORM_ACTIONS, that handler calls window.open(...) — but by that point the click's user-activation has been consumed by the async boundary, so Chrome/Safari/Firefox will frequently block the popup and custom-scheme navigation (codex://, cursor://, zed://) will silently fail. This is the exact "user activation lost across await" pattern. Only the copy-to-clipboard path is robust because clipboard writes work post-await in secure contexts.
Also, window.open can return null when blocked; nothing surfaces that to the user.
🛠️ One way to fix: open the window synchronously, then assign the URL
type AgentAction = {
key: string;
name: string;
icon: Component<JSX.SvgSVGAttributes<SVGSVGElement>>;
buttonIcon?: Component<JSX.SvgSVGAttributes<SVGSVGElement>>;
- execute: (prompt: string) => void;
+ execute: (prompt: string, win?: Window | null) => void;
+ opensWindow?: boolean;
};- {
- key: 'claude-code',
- name: 'Claude Code Web',
- icon: ClaudeIcon,
- execute: (prompt) =>
- window.open(
- `https://claude.ai/code?q=${encodeURIComponent(prompt)}`,
- '_blank'
- ),
- },
+ {
+ key: 'claude-code',
+ name: 'Claude Code Web',
+ icon: ClaudeIcon,
+ opensWindow: true,
+ execute: (prompt, win) => {
+ if (win) win.location.href = `https://claude.ai/code?q=${encodeURIComponent(prompt)}`;
+ },
+ },
// ...same for codex/cursor/zed const executeAction = async (action: AgentAction) => {
+ const win = action.opensWindow
+ ? window.open('about:blank', '_blank', 'noopener,noreferrer')
+ : null;
+ if (action.opensWindow && !win) {
+ toast.failure('Popup blocked — please allow popups for this site');
+ setOpen(false);
+ return;
+ }
try {
const prompt = await buildPrompt();
- action.execute(prompt);
+ action.execute(prompt, win);
setLastUsedKey(action.key);
} catch (e) {
+ win?.close();
console.error('Failed to generate task prompt', e);
toast.failure('Failed to generate task prompt');
}
setOpen(false);
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@js/app/packages/block-md/component/DispatchAgentMenu.tsx` around lines 196 -
206, The user-activation is lost across the await in executeAction causing
window.open in PLATFORM_ACTIONS to be blocked; fix by detecting
platform/open-in-IDE actions inside executeAction, synchronously call const win
= window.open('', '_blank') before await buildPrompt(), then await
buildPrompt(), pass the prompt (and the win reference if needed) to
action.execute(prompt, win) or set win.location.href to the final URL returned
by the action; if win is null (blocked) or the action fails to produce a URL,
show a toast.failure and avoid silent failure; keep setLastUsedKey(action.key)
and setOpen(false) behavior but ensure window.open happens before the async
boundary and that action.execute can accept or return the URL to assign to win.
CleanShot.2026-04-23.at.17.19.25.mp4