Skip to content

feat(dashboard): stop an agent from the board without opening its worktree - #16788

Closed
MumuTW wants to merge 1 commit into
stablyai:mainfrom
MumuTW:fix/dashboard-stop-agent
Closed

MumuTW wants to merge 1 commit into
stablyai:mainfrom
MumuTW:fix/dashboard-stop-agent

Conversation

@MumuTW

@MumuTW MumuTW commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Split out of #12095, which bundled this dashboard feature with an unrelated Antigravity quota rewrite. This PR is the dashboard half only — no src/main/rate-limits/ changes. Rebased onto current main.

Why

Clearing a finished agent off the kanban board required opening its worktree, finding the tab, and closing it there — the card only disappeared once the pane died. For idle agents, which is most of what accumulates on the board, that is three navigations to dismiss one card.

What

A per-card stop control in the top-right corner, where the state dot sits (the dot fades on hover so they never collide).

  • Idle and done cards confirm inline: the X becomes a red "Stop agent?" pill that commits on a second click and reverts on mouse-leave, blur, Escape, or a 3s timeout.
  • Anything still running routes through a modal reusing CloseTerminalDialog's strings verbatim, so the board and Cmd+W say the same thing.
  • No bulk "clear idle" action: idle means the agent is not working right now, not that its work is finished, so each dismissal stays deliberate.

Ordering and teardown

The row must be dropped before the pane is torn down. dropAgentStatus only plants its one-shot retention suppressor when a live entry still exists; reversed, the retention sync re-adds the agent as a retained "done" row and the card never leaves the board.

Two teardown cases the naive "kill the pty" would get wrong:

  • A split tab hosts panes the user did not ask to close, so kill only this agent's pty and leave the tab standing. Single-pane tabs go through closeTerminalTab for full Cmd+W semantics.
  • A pinned tab's close confirmation would open a modal in the main window, which is behind the board and may be unattended. Stop the agent anyway via the onCancel path and leave the pinned tab alone — a button that silently does nothing reads as broken.

The IPC deliberately does not raise the main window: the point is clearing an agent without leaving the board.

Structural notes

Two extractions the 300-line ceiling forced, both following existing patterns:

  • The kanban column moves out of AgentKanbanBoard into its own AgentKanbanColumn.
  • The stop-args validator lives in dashboard-stop-agent-validation.ts; dashboard-payload-validation.ts is at the ceiling, so it only re-exports — the same wiring dashboard-agent-launch-validation already uses.

ja.json uses Agent rather than エージェント, matching every other Agent string in that catalog and satisfying verify:localization-catalog.

Validation

pnpm typecheck, oxlint, the three verify:localization-* gates, check:reliability-gates, check:max-lines-ratchet, check:runtime-electron-ratchet, and audit:code-quality:native all pass. Scoped vitest run: 45 files, 409 tests passing.

https://claude.ai/code/session_01MZJuvDFhjHtuky5z9BF43a

@coderabbitai

coderabbitai Bot commented Aug 27, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: d3fbbb20-4141-4165-bcac-f5d819f95ed8

📥 Commits

Reviewing files that changed from the base of the PR and between 2a5ac4cbc3b4172e2a0e98e76d98a63c5fee38de and c9199c7.

📒 Files selected for processing (9)
  • src/main/ipc/dashboard-popout.test.ts
  • src/main/ipc/dashboard-popout.ts
  • src/preload/api/dashboard-bridge.ts
  • src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/i18n/locales/ko.json

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The change adds dashboard agent stopping. It defines and validates stop payloads, exposes stop methods through preload and IPC, and forwards requests to the trusted renderer. The dashboard adds inline and dialog confirmations. The renderer removes agent state and closes or kills the related terminal pane. Tests cover validation, IPC forwarding, UI interactions, live-card re-resolution, and teardown behavior.

Priority: ➖ Normal

Merge Risk: 🟡 Moderate · up to c9199

A stale stop confirmation can target a later card that reuses the same pane key, so this should be corrected before merge. Pinned-tab stops complete without requiring an inaccessible confirmation.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the change, motivation, behavior, teardown rules, and validation results. However, it omits required template sections, including a qualifying Linked Issue entry, Visual Proof… Complete the required template sections. Add the issue link in the required format, attach before-and-after visual proof or write N/A with a reason, complete the Testing section and platform coverage, address Review, Agent skill upstream bo…
Docstring Coverage ⚠️ Warning Docstring coverage is 42.11% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 18 files. (5 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: stopping an agent from the dashboard board without opening its worktree.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description explains the change, motivation, behavior, teardown rules, and validation results. However, it omits required template sections, including a qualifying Linked Issue entry, Visual Proof or an explicit N/A explanation, Testing checkboxes and platform coverage, Notes, Review, the upstream-boundary checklist, and the final checklist.

Resolution

Complete the required template sections. Add the issue link in the required format, attach before-and-after visual proof or write N/A with a reason, complete the Testing section and platform coverage, address Review, Agent skill upstream boundary, Notes, and Checklist items, and add AI disclosure details when applicable.

Full details: Docstring Coverage

Explanation

Docstring coverage is 42.11% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 19 functions across 18 files. (5 skipped: 5 unsupported.)

  • Fix all pre-merge checks with AI

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.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b2ccdf78-4bca-474d-92ad-a0186098be2e

📥 Commits

Reviewing files that changed from the base of the PR and between 026389a and 2a5ac4cbc3b4172e2a0e98e76d98a63c5fee38de.

📒 Files selected for processing (22)
  • src/main/ipc/dashboard-payload-validation.ts
  • src/main/ipc/dashboard-popout.test.ts
  • src/main/ipc/dashboard-popout.ts
  • src/main/ipc/dashboard-stop-agent-validation.ts
  • src/preload/api/dashboard-api.ts
  • src/preload/index.ts
  • src/renderer/src/components/dashboard-popout/AgentKanbanBoard.test.tsx
  • src/renderer/src/components/dashboard-popout/AgentKanbanBoard.tsx
  • src/renderer/src/components/dashboard-popout/AgentKanbanCard.test.tsx
  • src/renderer/src/components/dashboard-popout/AgentKanbanCard.tsx
  • src/renderer/src/components/dashboard-popout/AgentKanbanCardStopControl.tsx
  • src/renderer/src/components/dashboard-popout/AgentKanbanColumn.tsx
  • src/renderer/src/components/dashboard-popout/AgentStopConfirmDialog.tsx
  • src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/lib/stop-dashboard-agent.test.ts
  • src/renderer/src/lib/stop-dashboard-agent.ts
  • src/shared/dashboard-snapshot.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment on lines +178 to +184
const [pendingStopPaneKey, setPendingStopPaneKey] = useState<string | null>(null)
const resolvedPendingStopCard = useMemo(() => {
if (!pendingStopPaneKey) {
return null
}
return snapshot.cards.find((card) => card.paneKey === pendingStopPaneKey) ?? null
}, [pendingStopPaneKey, snapshot.cards])

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clear the pending stop key when its card disappears.

When snapshot.cards omits the pending paneKey, resolvedPendingStopCard becomes null, so the dialog closes but pendingStopPaneKey remains set. If a later snapshot reintroduces that pane, the dialog opens without a new stop action and can submit a stale confirmation. Clear the pending state when the live card is absent, and add a remove-then-readd regression test.

Proposed fix
   const resolvedPendingStopCard = useMemo(() => {
     if (!pendingStopPaneKey) {
       return null
     }
     return snapshot.cards.find((card) => card.paneKey === pendingStopPaneKey) ?? null
   }, [pendingStopPaneKey, snapshot.cards])

+  useEffect(() => {
+    if (pendingStopPaneKey !== null && resolvedPendingStopCard === null) {
+      setPendingStopPaneKey(null)
+    }
+  }, [pendingStopPaneKey, resolvedPendingStopCard])
📝 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.

Suggested change
const [pendingStopPaneKey, setPendingStopPaneKey] = useState<string | null>(null)
const resolvedPendingStopCard = useMemo(() => {
if (!pendingStopPaneKey) {
return null
}
return snapshot.cards.find((card) => card.paneKey === pendingStopPaneKey) ?? null
}, [pendingStopPaneKey, snapshot.cards])
const [pendingStopPaneKey, setPendingStopPaneKey] = useState<string | null>(null)
const resolvedPendingStopCard = useMemo(() => {
if (!pendingStopPaneKey) {
return null
}
return snapshot.cards.find((card) => card.paneKey === pendingStopPaneKey) ?? null
}, [pendingStopPaneKey, snapshot.cards])
useEffect(() => {
if (pendingStopPaneKey !== null && resolvedPendingStopCard === null) {
setPendingStopPaneKey(null)
}
}, [pendingStopPaneKey, resolvedPendingStopCard])

Comment on lines +47 to +55
closeTerminalTab(args.tabId, {
reason: 'user',
// Why: the pinned-close confirmation would open a modal in the main window,
// which is behind the board and may be unattended. Stop the agent anyway
// and leave the pinned tab itself alone — silently doing nothing would read
// as a broken button.
rejectPinned: true,
onCancel: () => killAgentPane(ptyId)
})

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Skip the duplicate running-process confirmation.

For a running unpinned agent, closeTerminalTab opens its own dialog because this call omits skipRunningProcessConfirm. The board already confirms this action, so the agent remains running until the user handles a second dialog in the main window. Pass skipRunningProcessConfirm: true.

Proposed fix
   closeTerminalTab(args.tabId, {
     reason: 'user',
+    skipRunningProcessConfirm: true,
     rejectPinned: true,
📝 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.

Suggested change
closeTerminalTab(args.tabId, {
reason: 'user',
// Why: the pinned-close confirmation would open a modal in the main window,
// which is behind the board and may be unattended. Stop the agent anyway
// and leave the pinned tab itself alone — silently doing nothing would read
// as a broken button.
rejectPinned: true,
onCancel: () => killAgentPane(ptyId)
})
closeTerminalTab(args.tabId, {
reason: 'user',
skipRunningProcessConfirm: true,
// Why: the pinned-close confirmation would open a modal in the main window,
// which is behind the board and may be unattended. Stop the agent anyway
// and leave the pinned tab itself alone — silently doing nothing would read
// as a broken button.
rejectPinned: true,
onCancel: () => killAgentPane(ptyId)
})

…ktree

Clearing a finished agent off the kanban board required opening its
worktree, finding the tab, and closing it there — the card only
disappeared once the pane died. For idle agents, which is most of what
accumulates on the board, that is three navigations to dismiss one card.

Add a per-card stop control in the top-right corner, where the state dot
sits (the dot fades on hover so they never collide). Idle and done cards
confirm inline: the X becomes a red "Stop agent?" pill that commits on a
second click and reverts on mouse-leave, blur, Escape, or a 3s timeout.
Anything still running routes through a modal reusing CloseTerminalDialog's
strings verbatim, so the board and Cmd+W say the same thing. No bulk
"clear idle" action: idle means the agent is not working right now, not
that its work is finished, so each dismissal stays deliberate.

The row must be dropped BEFORE the pane is torn down. dropAgentStatus only
plants its one-shot retention suppressor when a live entry still exists;
reversed, the retention sync re-adds the agent as a retained "done" row and
the card never leaves the board.

Two teardown cases the naive "kill the pty" would get wrong:

- A split tab hosts panes the user did not ask to close, so kill only this
  agent's pty and leave the tab standing. Single-pane tabs go through
  closeTerminalTab for full Cmd+W semantics.
- A pinned tab's close confirmation would open a modal in the main window,
  which is behind the board and may be unattended. Stop the agent anyway
  via the onCancel path and leave the pinned tab alone — a button that
  silently does nothing reads as broken.

The IPC deliberately does not raise the main window: the point is clearing
an agent without leaving the board.

The kanban column moves out of AgentKanbanBoard into its own
AgentKanbanColumn, and the stop-args validator into
dashboard-stop-agent-validation — dashboard-payload-validation is at the
300-line ceiling, so it only re-exports, matching how
dashboard-agent-launch-validation is already wired.

Claude-Session: https://claude.ai/code/session_01MZJuvDFhjHtuky5z9BF43a
@MumuTW
MumuTW force-pushed the fix/dashboard-stop-agent branch from 2a5ac4c to c9199c7 Compare September 12, 2026 05:42
@MumuTW

MumuTW commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto current main. Upstream split the preload monolith into api/*-bridge modules, so the stopAgent/onStopAgent wiring now lives in src/preload/api/dashboard-bridge.ts; index.ts is untouched. i18n conflicts were formatting-only — the stop card strings are re-applied on top of upstream's layout for all five locales.

@MumuTW

MumuTW commented Sep 12, 2026

Copy link
Copy Markdown
Contributor Author

Closing this PR. I am not going to keep iterating contribution PRs here; if a report is useful I will file an issue instead.

@MumuTW MumuTW closed this Sep 12, 2026
@MumuTW
MumuTW deleted the fix/dashboard-stop-agent branch September 12, 2026 13:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant