feat(site): expose workspace apps in chat workspace pill#24295
Conversation
mafredri
left a comment
There was a problem hiding this comment.
First-pass review (Netero). This is a mechanical review only; the full review panel has not yet evaluated this PR. These are defects the author should address before the panel spends parallel review time.
The approach of replacing hardcoded IDE items with a dynamic dropdown is clean, and the sub-component decomposition (VSCodeMenuItem, AppMenuItem, TerminalMenuItem) keeps each concern isolated. The Storybook coverage is solid.
3 P2, 1 P3, 1 Nit, 1 Note.
Netero on the filter: "The filter is agent.apps.filter((app) => !app.hidden). Web apps (code-server, JupyterLab) are not filtered out. TaskApps.tsx uses splitEmbeddedAndExternalApps for exactly this purpose."
🤖 This review was automatically generated with Coder Agents.
908246e to
eea8ae7
Compare
mafredri
left a comment
There was a problem hiding this comment.
Panel review (8 reviewers). The refactoring is solid: replacing hardcoded IDE items with a dynamic dropdown is the right move, sub-component decomposition is clean, and the Storybook coverage for the happy paths is thorough. The R1 Netero findings were addressed well, and the revised D4 decision to include all non-hidden apps was unanimously accepted by the panel.
1 P2, 8 P3, 2 P4, 4 Nit, 2 Note.
The P2 is a regression: AppMenuItem renders external app links as clickable before the API key query resolves, so session-token-bearing URLs get an empty token on cold cache. The established AppLink component gates this with !link.hasToken; the new code bypasses that guard entirely.
F3 (chatId gap for user-configured apps) was acknowledged as a known architectural limitation but deferred without a tracking ticket. If this is accepted as permanent, that should be stated explicitly. If not, a ticket should be filed.
Mafuuu on the popover contract: "Click an item, it fires, the popover dismisses. AppMenuItem has no onDone callback and never calls setOpen(false). After clicking a user-configured app, the popover stays open."
🤖 This review was automatically generated with Coder Agents.
Re: F3 (chatId gap for user-configured apps) — this is acknowledged as a known architectural limitation and is documented in the PR description. It's not intended as permanent — it should be addressed in a follow-up when the All other panel findings (C007–C021) have been addressed in commit 9b66100:
|
Replace the hardcoded 'Open in VS Code' and 'Open in Cursor' items in the chat meatball menu with a dynamic workspace pill dropdown that shows all configured workspace apps. The workspace pill in the chat input area now becomes a dropdown when the workspace has apps available. It shows: - Built-in VS Code / VS Code Insiders display apps (with on-demand API key generation) - User-configured external apps (JetBrains, Windsurf, Positron, etc.) via the existing useAppLink hook - Built-in terminal - A 'View Workspace' link at the bottom The chatId parameter is automatically injected into VS Code family protocol URLs (vscode:, vscode-insiders:, cursor:, windsurf:) so the Coder Remote extension can reconnect to the active chat session. When no apps are available (workspace not running, no agent), the pill falls back to a simple link to the workspace page. The meatball menu retains its management actions: Copy SSH Command, View Workspace, Generate Title, Archive/Delete.
Add stories covering: - WithAllApps: built-in IDEs + external apps + terminal in dropdown - WithBuiltinAppsOnly: only VS Code and terminal - WithExternalAppsOnly: only user-configured external apps - NoApps: plain link fallback when agent has no apps Play functions verify the dropdown opens and shows the correct items for each configuration.
Remove the custom chatId URL post-processing for external apps. Built-in display apps (VS Code, VS Code Insiders) still pass chatId through getVSCodeHref. User-configured external apps now use useAppLink as-is, matching how the dashboard's AgentRow handles them.
The component and function names are self-documenting. The JSDoc blocks were restating what the code already says and referencing external patterns (AgentRow) that could drift.
…tNode threading WorkspacePill now computes status icon/label internally from the workspace data instead of receiving pre-computed values. This removes the redundant decomposition in AgentChatPageView where workspace data was split into AttachedWorkspaceInfo fields then passed alongside the original workspace object. AgentChatInput now receives workspace/agent/chatId as data props and renders WorkspacePill directly, replacing the pre-rendered ReactNode that was threaded through two intermediate components.
The CompletedWithDiffPanel story asserted 'Open in Cursor' and 'Open in VS Code' in the meatball menu, which no longer has those items. Update to assert View Workspace and Archive Agent, and verify the IDE items are absent.
Switch from DropdownMenu to Popover with custom button-based menu items, matching the styling from PR #24308. Key changes: - Popover opens side='top' with 'w-48 p-1' container - ChevronDown icon with rotation animation on open - Custom button items with consistent hover/disabled states - Separator as a simple div with bg-border-default - Popover closes after each action via onDone callbacks
Move Copy SSH Command and View Workspace out of the ChatTopBar meatball menu into the WorkspacePill popover. The meatball menu now only contains Generate Title and Archive/Delete actions. Remove the WorkspaceActions interface from ChatTopBar entirely. WorkspacePill returns null when there are no apps and no SSH command, letting the ToolBadge fallback handle the simple link case.
The filter was using isExternalApp which excluded web apps like Mux that use path-based or subdomain routing. useAppLink already handles all app types, so just filter on !hidden.
…ard, nit)
- Thread gitWatcher-preferred folder through to VSCodeMenuItem for
correct monorepo folder resolution (C002)
- Remove early return null in WorkspacePill so the pill always renders
when workspace data is available (C004)
- Remove stray {" "} whitespace in ChatTopBar (C005)
- Extract getWorkspaceStatusDisplay into shared
WorkspaceStatusIndicator to deduplicate status icon/label logic
between WorkspacePill and AgentChatPageView (C006)
…ies, tests)
- Migrate WorkspacePill from Popover to DropdownMenu for proper a11y
(role=menu, role=menuitem, arrow-key navigation) (C010)
- Add session token guard to AppMenuItem — disable href until API key
resolves for apps with $SESSION_TOKEN (C007)
- Remove onDone callbacks — DropdownMenu auto-closes on select (C008)
- Add disabled={isPending} to VSCodeMenuItem button (C009)
- Rename stale WithWorkspaceActions story to WithWorkspace (C011)
- Add play function to NoApps story verifying View Workspace (C012)
- Add hidden app fixture and WithHiddenApp story (C013)
- Add sshCommand to WithAllApps story and assertion (C014)
- Remove unused effectiveType from getWorkspaceStatusDisplay (C015)
- Add vitest for getWorkspaceStatusDisplay branches (C016)
- Conditionally render DropdownMenuSeparator (C017)
- Move badgeCls to module level (C018)
- Rename WorkspaceStatusIndicator.tsx to workspaceStatusDisplay.tsx (C019)
- Align chevron rotation to convention with comment (C020)
- Fix ExternalAppMenuItem typo in story comment (C021)
…tate, a11y)
- Move useMutation onSuccess/onError to config object so callbacks
fire even after DropdownMenu portal unmounts (P2)
- Add disabled={!canClick} to AppMenuItem when session token is
still loading (P3)
- Add aria-label to dropdown trigger button (P3)
…, running guard) - Use fully controlled tooltip state to avoid Radix controlled/ uncontrolled oscillation warning - Add alt="" to ExternalImage in AppMenuItem (decorative, a11y) - Change preferredFolder from ?? to || so empty-string repo roots fall through to expanded_directory - Disable VS Code, app, and terminal menu items when workspace is not running
- Inline badgeCls directly into cn() call in WorkspacePill - Remove workspaceStatusDisplay.tsx and its test file - Inline status icon/label computation into both callers (WorkspacePill and AgentChatPageView) - Replace iconCls variable with direct "size-3" on each icon
…on map Extract a StatusIcon component that maps DisplayWorkspaceStatusType to the corresponding monitor icon. Replaces the duplicated statusIconMap in both WorkspacePill and AgentChatPageView.
d56099a to
19ba116
Compare
mafredri
left a comment
There was a problem hiding this comment.
Re-review (4 reviewers). Excellent fix round: all 16 posted findings from R2 addressed in a single commit (9b66100). The DropdownMenu migration resolves F12 (popover close) and F16 (accessibility) together, and the session-token guard (F11) is properly ported from AppLink.
F3 (chatId gap for user-configured apps) remains deferred without a tracking ticket. This needs a human decision: file a ticket or explicitly accept the gap as permanent. The author-agent cannot make that call.
F21 (badgeCls duplication) closed by the panel: the DropdownMenu migration eliminated the named variable, and the remaining inlined string is grepable at P4 risk.
1 P2, 2 P3, 3 Nit new this round.
The P2 is the same class of duplication as R1's F6: the ~25-line workspace status computation block (getDisplayWorkspaceStatus, agentPreparing, agentStartupFailed, effectiveType, statusLabel) is now duplicated verbatim between WorkspacePill.tsx:65 and AgentChatPageView.tsx:273. F6's icon-map duplication was extracted into StatusIcon.tsx, but the surrounding computation was not.
Hisoka on the mutation guard: "The new code moves this mutation inside VSCodeMenuItem, which Radix portals and unmounts when the menu closes. A fresh VSCodeMenuItem mounts with isPending: false."
🤖 This review was automatically generated with Coder Agents.
…stories, nits) - Extract getWorkspaceStatus() into StatusIcon.tsx to deduplicate the ~25-line status computation between WorkspacePill and AgentChatPageView (C022) - Lift useMutation to WorkspacePill parent so isPending persists across dropdown open/close cycles (C023) - Add WithStoppedWorkspace story asserting disabled items (C024) - Fix chevron comment direction (C025) - Remove unnecessary ?. on non-optional agent prop (C026) - Fix double 'open' in toast message — label is now 'VS Code' with 'Open in' prefix in JSX (C027)
mafredri
left a comment
There was a problem hiding this comment.
Re-review (4 reviewers). All R3 findings addressed cleanly in a95bf52. The getWorkspaceStatus() extraction (F25) and mutation lift to WorkspacePill (F27) are architecturally correct. Meruem's structural analysis confirmed the design: token gating, DropdownMenu usage, mutation lifecycle, tooltip/dropdown coordination, and isRunning gate are all sound.
Mafu-san on the fix quality: the agent generalized from F6 (icon-map duplication) to F25 (full status computation duplication) properly the second time, fixing the class of bug rather than the instance. No scope drift in the final commit.
1 P3, 1 Nit. Bisky found the incomplete half of F26: the stopped-workspace story asserts disabled on built-in apps but not external apps.
F3 (chatId gap for user-configured apps) remains deferred without a ticket. This needs a human decision.
Bisky: "If someone removes || !isRunning from AppMenuItem.disabled, clicking an external app on a stopped workspace navigates to a dead protocol URL and nothing catches it."
🤖 This review was automatically generated with Coder Agents.
… remove story JSDoc - Add aria-disabled assertions for JetBrains Gateway and Cursor in WithStoppedWorkspace play function (C028). - Remove all JSDoc comments from WorkspacePill.stories.tsx and the one PR-modified JSDoc from AgentChatPageView.stories.tsx. The const/export names describe stories sufficiently.
mafredri
left a comment
There was a problem hiding this comment.
Re-review (4 reviewers). F31/F32 addressed. Mafuuu and Hisoka found no issues. Hisoka probed shared mutation state, disabled anchor navigation, guard conditions, and meatball menu structure; all solid.
2 P3 new this round. No P2+.
F3 (chatId gap for user-configured apps) has been deferred without a ticket for 5 review rounds. This needs a human decision: file a ticket or explicitly accept the gap as permanent. The author-agent documented the gap in the PR description but cannot make that call.
Hisoka: "Five rounds. Thirty-two findings. All addressed or closed. The code held up."
🤖 This review was automatically generated with Coder Agents.
…abled assertion - WithWorkspace now passes workspace and workspaceAgent props so the workspace pill actually renders (F33). - WithAllApps asserts VS Code menu item is not aria-disabled, guarding against unconditional disabling regressions (F34).
mafredri
left a comment
There was a problem hiding this comment.
Re-review (4 reviewers). F33/F34 addressed. Mafuuu, Pariston, and Knov found no issues. Mafuuu verified all contracts (status, disabled, lifecycle, error). Pariston confirmed problem-solution alignment: the fix is at the right causal level, and the isRunning check is an improvement over the old code which let users click VS Code on stopped workspaces.
2 P3 new this round.
Netero caught that CopySSHMenuItem uses raw navigator.clipboard.writeText instead of the existing useClipboard hook, which provides an HTTP fallback. Bisky found that the stopped-workspace disabled assertions on external apps pass by accident: the token is never mocked, so canClick is always false regardless of workspace state.
F3 (chatId gap) remains deferred without a ticket. This needs a human decision.
Pariston on the Cursor transition: "Cursor is not a DisplayApp in the API; giving it built-in treatment was a shortcut. The direction is correct."
🤖 This review was automatically generated with Coder Agents.
…e assertions - Replace raw navigator.clipboard.writeText in CopySSHMenuItem with useClipboard hook for HTTP-only deployment fallback (F35). - Restore built-in Cursor menu item with chatId injection via getVSCodeHref, fixing regression from meatball menu removal. - Add parameters.queries mock for API key so external app disabled assertions in stories actually test the !isRunning guard (F36). - Add Cursor coverage across all stories (presence + disabled state).
mafredri
left a comment
There was a problem hiding this comment.
Re-review (4 reviewers). F35/F36 addressed. The API key query mock (F36 fix) was verified by Bisky: all 47 tests pass, disabled-state assertions are now genuine, and the mock key matches the apiKey() query key from users.ts:168.
2 P2, 2 P3 new.
F37 (P2, Netero) was independently confirmed by Mafuuu through contract tracing: useClipboard().copyToClipboard catches all errors internally and always resolves, so the .then() fires unconditionally.
Hisoka found that the hardcoded Cursor entry (line 144, unconditional VSCodeMenuItem) coexists with user-configured Cursor apps in the same dropdown. The hardcoded one has chatId via getVSCodeHref; the user-configured one (with the Cursor icon) does not. Users click the one that looks like Cursor, which is the one without chatId.
F3 (chatId gap) remains deferred without a ticket. This needs a human decision.
Hisoka: "Users click the one that looks like Cursor. That is the one without chatId. The Coder Remote extension opens the workspace but cannot reconnect to the chat session. No error, no indication of what was lost."
🤖 This review was automatically generated with Coder Agents.
…href/error - Remove hardcoded Cursor VSCodeMenuItem to avoid duplication with user-configured Cursor apps. chatId gap tracked in #24331 (F38). - Remove .then() on copyToClipboard since the hook never rejects. Contradictory success+error toasts on failure (F37). - Tighten AppMenuItem href guard to include isRunning so disabled items have no live href (F39). - Widen onError to accept error parameter, use getErrorMessage for the toast so users see the actual error (F40).
Tracked in #24331. |
mafredri
left a comment
There was a problem hiding this comment.
Status: APPROVED
APPROVE. Eight rounds, 41 findings, 34 fixed, 2 contested and panel-closed, 1 deferred with ticket (#24331), 2 Notes (informational), 1 P3 remaining (test assertion depth). The code is solid.
F37-F40 all addressed in 95d78c5. Mafuuu verified all four fixes through contract tracing, lifecycle analysis, and adversarial testing. Hisoka and Kite found no issues. The hardcoded Cursor removal (F38) was clean: variant type narrowed, no orphaned references, hasItemsAboveSeparator adapts correctly.
F3 (chatId gap) now has ticket #24331. The long-standing deferral-without-ticket concern is resolved.
1 P3 remaining: WithStoppedWorkspace asserts aria-disabled but not href absence, so the F39 fix is untested. Low risk.
Kite: "Well-decomposed extraction. Each sub-component owns exactly one concern. The shared useMutation for API key generation prevents double-fire across VS Code/Insiders."
🤖 This review was automatically generated with Coder Agents.
…nal apps
Add not.toHaveAttribute('href') assertions for JetBrains Gateway and
Cursor in WithStoppedWorkspace to verify the F39 href guard (F41).
Move Copy SSH Command below the separator so launch items (VS Code, user apps, terminal) are grouped together. Copy SSH and View Workspace form the utility/navigation group below.
… prefix Drop 'Open in' from VS Code/VS Code Insiders and 'Open' from Terminal labels to match user-configured app naming (just the app name). All menu items now consistently show their name only.
mafredri
left a comment
There was a problem hiding this comment.
Only one gotcha, although I think there's a larger issue at play where we really need to update our Git watcher with support for switching project/folder and better detection in-chat which one is relevant. As a user of 1-workspace-inf-chats the diff viewer is never showing the right thing.
But essentially, this begs to question what selection the app should choose. The one shown in diff viewer? Give the user options?
This might be a new feature, i.e. allowing to set "workdir" for a chat even after-the-fact (different from template configured).
| // Prefer the git repository root over the agent's expanded directory | ||
| // for VS Code folder resolution (important for monorepos). | ||
| const preferredFolder = (() => { | ||
| const repoRoots = Array.from(gitWatcher?.repositories.keys() ?? []).sort(); |
There was a problem hiding this comment.
This seems like it could have unexpected consequences if gitWatcher is tracking multiple repositories? Your diff view shows one repo and then the editor or what not opens up in another.
There was a problem hiding this comment.
Definitely agree with you although that's the existing behavior.
- // Prefer the active git repo root so VS Code opens to the
- // actual project directory, falling back to the agent's
- // configured directory.
- const repoRoots = Array.from(gitWatcher.repositories.keys()).sort();
- const folder = repoRoots[0] ?? workspaceAgent.expanded_directory;We should definitely noodle on a better approach here.
Note
🤖 This PR was created by Coder Agent on behalf of Danielle Maywood
Replaces the hardcoded "Open in VS Code" and "Open in Cursor" items in the chat meatball menu with a dynamic workspace pill dropdown that shows all configured workspace apps.
The workspace pill in the chat input area now becomes a dropdown when the workspace has apps available. It shows:
chatIdinjection)useAppLinkhookThe pill always renders when workspace data is available (even if no apps are configured), since the popover always includes "View Workspace" as a minimum action.
VS Code folder resolution prefers the active git repository root (from
gitWatcher) over the agent'sexpanded_directory, preserving correct behavior for monorepos.The
chatIdparameter is injected into built-in VS Code and VS Code Insiders display apps viagetVSCodeHref. User-configured external apps (including Cursor/Windsurf) do not currently receivechatId— this is a known gap that can be addressed in a follow-up.The meatball menu retains its management actions: Generate Title, Archive/Delete.
Closes CODAGT-113