Skip to content

feat(dashboard): allow closing CLI sessions from the dashboard - #20377

Closed
MumuTW wants to merge 1 commit into
stablyai:mainfrom
MumuTW:MumuTW/close-agent-from-dashboard
Closed

MumuTW wants to merge 1 commit into
stablyai:mainfrom
MumuTW:MumuTW/close-agent-from-dashboard

Conversation

@MumuTW

@MumuTW MumuTW commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Lets you close a CLI agent session from the dashboard / sidebar instead of only from the session itself.

  • Close action on dashboard agent rows and worktree agent cards
  • Confirmation dialog + shared use-agent-session-close hook
  • i18n (en/es/ja/ko/zh)
  • e2e screenshot coverage for the close flow

This is on top of the earlier stop-from-board work; it is the close-session path (CloseAgentSessionDialog, close-dashboard-agent-session.ts), not the Antigravity quota rebuild in #12095.

Test plan

  • From the dashboard, close a running CLI session and confirm it actually ends
  • Cancel in the confirmation dialog leaves the session running
  • Sidebar worktree agent card close matches dashboard close
  • e2e tests/e2e/dashboard-close-agent-screenshot.spec.ts

@pullfrog

pullfrog Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Run failed. View the logs →

Pullfrog  | Rerun failed job ➔ | View workflow run | via Pullfrog | Using openrouter/deepseek/deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

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

coderabbitai Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

📝 Walkthrough

Walkthrough

Adds dashboard agent-session closure across sidebar and popout views. The change defines and validates a shared close request, transports it through preload and IPC, and handles it in the main renderer. Live sessions show confirmation before closing the full terminal tab. Dead sessions are dismissed directly. Sidebar and popout controls, translations, unit tests, and an end-to-end screenshot spec are included.

Priority: ⚪ Not assessed

Merge Risk: 🟡 Moderate · up to 0521b

Closing a retained agent card can unexpectedly close an entire terminal tab and stop work in its split panes. This destructive behavior should be corrected before merge; the remaining issues affect accessibility, dialog polish, and localized close-session UI.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the main change and lists a test plan, but it omits most required template sections, including ELI5, What Changed, Why, Linked Issue with an issue number, Visual Proof, Testin… Update the description to use the repository template. Add the missing required sections, link the issue in Fixes #..., provide visual proof or state N/A with a reason, document testing and platform coverage, and complete the checklist.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 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 primary change: enabling CLI session closure from the dashboard.
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 main change and lists a test plan, but it omits most required template sections, including ELI5, What Changed, Why, Linked Issue with an issue number, Visual Proof, Testing checkboxes, AI Disclosure, Review, Agent skill upstream boundary, Notes, and Checklist.

Full details: Docstring Coverage

Explanation

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

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
⚔️ Resolve merge conflicts 💡
  • Resolve merge conflict in branch MumuTW/close-agent-from-dashboard

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


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 554c98c1-6f6d-48a3-835d-2a793728baa1

📥 Commits

Reviewing files that changed from the base of the PR and between b357913 and 0521b0a.

📒 Files selected for processing (23)
  • src/main/ipc/dashboard-payload-validation.ts
  • src/main/ipc/dashboard-popout.test.ts
  • src/main/ipc/dashboard-popout.ts
  • src/preload/api-types.ts
  • src/preload/index.ts
  • 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/DashboardAgentRow.tsx
  • src/renderer/src/components/dashboard/DashboardAgentRowTrailingControls.tsx
  • src/renderer/src/components/dashboard/close-dashboard-agent-session.ts
  • src/renderer/src/components/dashboard/useDashboardPopoutBridge.test.tsx
  • src/renderer/src/components/dashboard/useDashboardPopoutBridge.ts
  • src/renderer/src/components/sidebar/CloseAgentSessionDialog.tsx
  • src/renderer/src/components/sidebar/WorktreeCardAgents.tsx
  • src/renderer/src/components/sidebar/use-agent-session-close.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/shared/dashboard-snapshot.ts
  • tests/e2e/dashboard-close-agent-screenshot.spec.ts

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

const [closeTarget, setCloseTarget] = useState<DashboardCard | null>(null)
const handleCloseCard = useCallback((card: DashboardCard) => {
if (card.ptyId === null) {
void window.api.dashboard.closeAgent?.({ paneKey: card.paneKey, tabId: card.tabId })

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

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 6 'ptyId: null|ptyId.*null|tabId:.*tabId|retainedAgents' \
  src/renderer/src/components/dashboard

Repository: stablyai/orca

Length of output: 44631


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- AgentKanbanBoard dismissal branch ---'
sed -n '205,250p' src/renderer/src/components/dashboard-popout/AgentKanbanBoard.tsx

printf '%s\n' '--- snapshot row construction ---'
sed -n '230,315p' src/renderer/src/components/dashboard/build-dashboard-snapshot.ts

printf '%s\n' '--- closeAgent bindings and implementation locations ---'
rg -n -C 5 'closeDashboardAgentSession|closeAgent\??:|closeAgent:' src | head -240

Repository: stablyai/orca

Length of output: 13112


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- close session implementation ---'
cat -n src/renderer/src/components/dashboard/close-dashboard-agent-session.ts

printf '%s\n' '--- retained row construction ---'
sed -n '1,220p' src/renderer/src/components/dashboard/build-dashboard-snapshot.ts
sed -n '214,310p' src/renderer/src/components/dashboard/useRetainedAgents.ts

printf '%s\n' '--- close API type ---'
rg -n -C 5 'DashboardCloseAgentArgs' src shared

printf '%s\n' '--- focused snapshot tests around retained rows and PTY liveness ---'
rg -n -C 12 'retained|ptyIdsByTabId|ptyIdsByLeafId|split|dead|gone' \
  src/renderer/src/components/dashboard/build-dashboard-snapshot*.test.ts \
  src/renderer/src/components/dashboard/close-dashboard-agent-session*.test.ts \
  src/renderer/src/components/dashboard/useRetainedAgents*.test.ts

Repository: stablyai/orca

Length of output: 23922


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- row builder and retained selector locations ---'
rg -n -C 4 'function buildWorktreeAgentRows|export function buildWorktreeAgentRows|selectRetainedAgentEntriesForWorktree|type RetainedAgentEntry' \
  src/renderer/src/components/sidebar src/shared

printf '%s\n' '--- row builder source ---'
builder=$(rg -l 'function buildWorktreeAgentRows|export function buildWorktreeAgentRows' src/renderer/src/components/sidebar | head -1)
if [ -n "$builder" ]; then
  cat -n "$builder"
fi

printf '%s\n' '--- retained selector source ---'
selector=$(rg -l 'selectRetainedAgentEntriesForWorktree' src/renderer/src/components/sidebar | head -1)
if [ -n "$selector" ]; then
  cat -n "$selector"
fi

printf '%s\n' '--- focused tests ---'
rg -n -C 12 'retained done|retained.*tab|ptyIdsByTabId|ptyIdsByLeafId|split|gone|dead state' \
  src/renderer/src/components/dashboard/build-dashboard-snapshot*.test.ts \
  src/renderer/src/components/sidebar/*agent-row*.test.ts \
  src/renderer/src/components/dashboard/close-dashboard-agent-session*.test.ts 2>/dev/null || true

Repository: stablyai/orca

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -u

printf '%s\n' '--- close target resolver ---'
cat -n src/renderer/src/components/terminal/terminal-close-target.ts

printf '%s\n' '--- retained snapshot test ---'
sed -n '238,260p' src/renderer/src/components/dashboard/build-dashboard-snapshot.test.ts
sed -n '318,350p' src/renderer/src/components/dashboard/build-dashboard-snapshot.test.ts

printf '%s\n' '--- relevant terminal tab types and live PTY selectors ---'
rg -n -C 5 'type TerminalTab|interface TerminalTab|selectLivePtyIdsForWorktree|ptyIdsByTabId' \
  src/shared/types.ts src/renderer/src/components/sidebar/worktree-card-status-inputs.ts

Repository: stablyai/orca

Length of output: 8325


Pass null when dismissing cards without a live PTY.

buildDashboardSnapshot can emit a retained card with ptyId: null while tabId remains resolvable. The current call then reaches closeDashboardAgentSession, which closes the whole terminal tab, including split panes and sibling agents, instead of dismissing only the retained row.

-      void window.api.dashboard.closeAgent?.({ paneKey: card.paneKey, tabId: card.tabId })
+      void window.api.dashboard.closeAgent?.({ paneKey: card.paneKey, tabId: null })
📝 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
void window.api.dashboard.closeAgent?.({ paneKey: card.paneKey, tabId: card.tabId })
void window.api.dashboard.closeAgent?.({ paneKey: card.paneKey, tabId: null })

Comment on lines +276 to +282
<span
role="button"
tabIndex={0}
aria-label={translate('dashboardPopout.card.closeSession', 'Close session')}
title={translate('dashboardPopout.card.closeSession', 'Close session')}
onClick={handleCloseClick}
onKeyDown={handleCloseKeyDown}

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

Move the close control outside the card button.

Line 276 creates an interactive control inside the native button at line 246. This is invalid interactive-content nesting. Assistive technology and keyboard activation can produce inconsistent results.

Make the close button a sibling of the card button. Keep its click and key handling isolated.

Comment on lines +27 to +54
<Dialog open={open} onOpenChange={onOpenChange}>
{open ? (
<DialogContent className="max-w-sm sm:max-w-sm" showCloseButton={false}>
<DialogHeader>
<DialogTitle className="text-sm">
{translate(
'auto.components.sidebar.WorktreeCardAgents.2ec931a922',
'Close agent session?'
)}
</DialogTitle>
<DialogDescription className="text-xs">
{translate(
'auto.components.sidebar.WorktreeCardAgents.e1250e23e9',
'This closes the entire terminal tab running this agent, including all of its split panes. Any other agents running in those panes will be stopped too.'
)}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={() => onOpenChange(false)}>
{translate('auto.components.sidebar.WorktreeCardAgents.7d17689ff0', 'Cancel')}
</Button>
<Button variant="destructive" onClick={onConfirm}>
{translate('auto.components.sidebar.WorktreeCardAgents.6ac88e0ff9', 'Close session')}
</Button>
</DialogFooter>
</DialogContent>
) : null}
</Dialog>

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

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
# Description: Find other Dialog usages and check whether DialogContent is
# gated behind an `open ? ... : null` ternary or rendered unconditionally.
set -euo pipefail

echo "== Files gating DialogContent behind an open-ternary =="
rg -nP '\{\s*(open|.*[Oo]pen)\s*\?\s*\(' -g '*.tsx' src/renderer/src | rg -B2 'DialogContent' || true

echo
echo "== Sample of Dialog usages that render DialogContent unconditionally =="
rg -n -B1 -A1 '<Dialog ' -g '*.tsx' src/renderer/src | rg -A3 'DialogContent'

Repository: stablyai/orca

Length of output: 19989


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Reviewed component imports and render =="
cat -n src/renderer/src/components/sidebar/CloseAgentSessionDialog.tsx

echo
echo "== Local dialog primitive definitions =="
fd -i 'dialog.tsx' src/renderer/src
DIALOG_FILE="$(fd -i 'dialog.tsx' src/renderer/src | head -n 1)"
cat -n "$DIALOG_FILE" | sed -n '1,240p'

echo
echo "== Dialog dependency declarations =="
rg -n '"`@radix-ui/react-dialog`"|"radix-ui"' package.json src package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null | head -80 || true

Repository: stablyai/orca

Length of output: 17237


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Exact local dialog primitive =="
wc -l src/renderer/src/components/ui/dialog.tsx
cat -n src/renderer/src/components/ui/dialog.tsx

echo
echo "== Relevant dependency metadata =="
sed -n '200,225p' package.json
rg -n -A8 -B3 '"radix-ui"' pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null | head -80 || true

echo
echo "== Direct-child dialog examples =="
cat -n src/renderer/src/components/sidebar/RemoveFolderDialog.tsx | sed -n '65,95p'

Repository: stablyai/orca

Length of output: 8986


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo "== Lockfiles =="
git ls-files | rg '(^|/)(pnpm-lock\.yaml|package-lock\.json|yarn\.lock|bun\.lockb?|npm-shrinkwrap\.json)$' || true
echo
echo "== Resolved radix-ui entries =="
for f in pnpm-lock.yaml package-lock.json yarn.lock bun.lock bun.lockb npm-shrinkwrap.json; do
  if [ -f "$f" ]; then
    echo "-- $f"
    rg -n -A4 -B2 'radix-ui' "$f" | head -80 || true
  fi
done

Repository: stablyai/orca

Length of output: 3460


🌐 Web query:

radix-ui 1.6.2 Dialog Content Presence source

💡 Result:

<search_synthesis>
In Radix UI, including version 1.6.2, component presence is managed internally by the @radix-ui/react-presence utility [1][2]. This utility enables support for exit animations by allowing components to remain mounted in the DOM after their state changes to "closed," until any associated CSS animations or transitions have completed [3][4]. Key points regarding Dialog Content presence: 1. Internal Presence Management: The Dialog component uses the Presence utility to track its mount/unmount lifecycle [3]. When the open state changes to false, the component monitors for CSS animation endings (via animationend, animationcancel, and animationstart events) to determine when to safely remove the content from the DOM [3]. 2. CSS Animations: You can trigger exit animations by targeting the [data-state="closed"] attribute on the DialogContent component [4]. The presence utility detects the animation and suspends unmounting until the animation completes [3][4]. 3. forceMount Prop: For scenarios where you need to manage presence manually—such as when using third-party animation libraries like Framer Motion or React Spring—Radix UI exposes the forceMount prop on Dialog.Content and Dialog.Overlay [5][6][4]. When forceMount is used, the component will remain mounted regardless of the open state, shifting the responsibility of mounting and unmounting the element to your code [6][4]. 4. Implementation Note: If wrapping Dialog.Content in custom elements, ensure the wrapper does not interfere with the detection of animations, as the presence utility relies on reading styles directly from the component being unmounted [3][7]. Using the asChild prop is often recommended to maintain the correct DOM structure and animation behavior [5][7].
</search_synthesis>

<source_evidence>

<title>packages/react/dialog/CHANGELOG.md</title> https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md # packages/react/dialog/CHANGELOG.md - Branch: main - Repository: radix-ui/primitives --- # `@radix-ui/react-dialog` ## 1.1.23 - Reverted breaking changes that caused compatibility issues with React Server Components. - Updated dependencies: `@radix-ui/react-compose-refs@1.1.5`, `@radix-ui/react-context@1.2.2`, `@radix-ui/react-dismissable-layer@1.1.19`, `@radix-ui/react-focus-guards@1.1.6`, `@radix-ui/react-focus-scope@1.1.16`, `@radix-ui/react-id@1.1.4`, `@radix-ui/react-portal@1.1.17`, `@radix-ui/react-presence@1.1.10`, `@radix-ui/react-primitive@2.1.10`, `@radix-ui/react-slot@1.3.3`, `@radix-ui/react-use-controllable-state@1.2.6`, `@radix-ui/react-use-layout-effect@1.1.4` ## 1.1.22 - Updated dependencies: `@radix-ui/react-slot@1.3.2`, `@radix-ui/react-primitive@2.1.9`, `@radix-ui/react-dismissable-layer@1.1.18`, `@radix-ui/react-focus-scope@1.1.15`, `@radix-ui/react-portal@1.1.16` ## 1.1.21 - Republish through CI to attach provenance attestations. The previous versions of these packages were published manually outside of CI and therefore shipped without provenance; this patch re-releases the same code through the CI pipeline so every package includes an attestation. - Updated dependencies: `@radix-ui/primitive@1.1.7`, `@radix-ui/react-compose-refs@1.1.4`, `@radix-ui/react-context@1.2.1`, `@radix-ui/react-dismissable-layer@1.1.17`, `@radix-ui/react-focus-guards@1.1.5`, `@radix-ui/react-focus-scope@1.1.14`, `@radix-ui/react-id@1.1.3`, `@radix-ui/react-portal@1.1.15`, `@radix-ui/react-presence@1.1.9`, `@radix-ui/react-primitive@2.1.8`, `@radix-ui/react-slot@1.3.1`, `@radix-ui/react-use-controllable-state@1.2.5`, `@radix-ui/react-use-layout-effect@1.1.3` ## 1.1.20 - Fixed broken ARIA references in Dialogs where a title or description elements are not rendered. - Improved tree-shaking so bundlers can drop unused components. Component parts are now marked `/* `@__PURE__` */` and use named render functions instead of `Component.displayName = ...` assignments, which previously prevented dead-code elimination with some bundlers. - Updated dependencies: `@radix-ui/react-dismissable-layer@1.1.16`, `@radix-ui/react-focus-scope@1.1.13`, `@radix-ui/react-portal@1.1.14`, `@radix-ui/react-presence@1.1.8`, `@radix-ui/react-use-controllable-state@1.2.4`, `@radix-ui/primitive@1.1.6`, `@radix-ui/react-compose-refs@1.1.3`, `@radix-ui/react-context@1.2.0`, `@radix-ui/react-focus-guards@1.1.4`, `@radix-ui/react-id@1.1.2`, `@radix-ui/react-primitive@2.1.7`, `@radix-ui/react-slot@1.3.0`, `@radix-ui/react-use-layout-effect@1.1.2` ## 1.1.19 - Updated dependencies: `@radix-ui/react-dismissable-layer@1.1.15`, `@radix-ui/primitive@1.1.5`, `@radix-ui/react-context@1.2.0`, `@radix-ui/react-focus-scope@1.1.12`, `@radix-ui/react-presence@1.1.7` ## 1.1.18 - Updated dependencies: `@radix-ui/react-primitive@2.1.7`, `@radix-ui/react-dismissable-layer@1.1.14`, `@radix-ui/react-focus-scope@1.1.11`, `@radix-ui/react-portal@1.1.13` ## 1.1.17 - Removed dev-only warnings for dialogs when title and/or description is not rendered. - Fixed Dismissable Layer so outside interactions stopped by extension UI overlays do not dismiss dialogs or popovers. - Updated dependencies: `@radix-ui/react-slot@1.3.0`, `@radix-ui/react-dismissable-layer@1.1.13`, `@radix-ui/react-primitive@2.1.6`, `@radix-ui/react-focus-scope@1.1.10`, `@radix-ui/react-portal@1.1.12` ## 1.1.16 - Fixed disabled pointer events in closed dialogs - Fixed a bug where iOS text selection and editing on HTML inputs within `react-dialog` were broken - Fixed triggers referencing a non-existent element via `aria-controls` when their content is removed from the DOM (credit to [`@dodomorandi`](https://github.com/dodomorandi) for the [original PR](https://github.com/radix-ui/primitives/pull/3243)) - Added repository.directory to all package.json files - Updated dependencies: `@radix-ui/react-presence@1.1.6`, `@radix-ui/react-slot@1.2.5`, `@radix-ui/react-focus-guards@1.1.4`, `@radix-ui/react-dismissable-layer@1.1.12`, `@r... <title>primitives/packages/react/presence at main · radix-ui/primitives · GitHub</title> https://github.com/radix-ui/primitives/tree/main/packages/react/presence primitives/packages/react/presence at main · radix-ui/primitives · GitHub / primitives Public Star 18.7k - Pricing - Notifications - Fork 1.1k ## Files main # presence History main # presence Top Last commit message Last commit date src src CHANGELOG.md CHANGELOG.md README.md README.md eslint.config.mjs eslint.config.mjs package.json package.json tsconfig.json tsconfig.json View all files | Name | Name | | --- | --- | | .. | ## README.md # react-presence This is an internal utility, not intended for public usage. <title>Result 3</title> https://radix-ui-primitives.mintlify.app/api/presence > Animates components when they mount and unmount, waiting for CSS animations to complete before unmounting. ... `Presence` is a component that helps you animate elements in and out of the DOM. It keeps elements mounted during exit animations and provides a `present` state to child components, enabling smooth mount and unmount animations. ... ```tsx interface PresenceProps { children: React.ReactElement | ((props: { present: boolean }) => React.ReactElement); present: boolean; } ``` ... Whether the component should be present (mounted) in the DOM. When `false`, the component remains mounted until exit animations complete. ... The element to animate. Can be a React element or a render function that receives `present` state. ... ### Conditional Content Based on State ... function ConditionalExample() ... const [isOpen, setIsOpen ... useState(false); return ( <> <button onClick={() => setIsOpen(!isOpen)}>Toggle</button> <Presence present={isOpen}> {({ present }) => ( <div className="panel"> {present ? ( <div>Content is visible</div> ) : ( <div>Animating out...</div> )} </div> )} </Presence ... </> ... ## How It Works ... `Presence` uses a state machine with three states: ... 1. mounted: Component is present and visible 2. unmountSuspended: Component is animating out (exit animation) 3. unmounted: Component is removed from DOM ... ### State Transitions ... - When `present` changes from `false` to `true`: `unmounted` → `mounted` - When `present` changes from `true` to `false`: - If CSS animation detected: `mounted` → `unmountSuspended` - When animation ends: `unmountSuspended` → `unmounted` - If no animation: `mounted` → `unmounted` (immediate) ... ### Animation Detection ... The component: ... - Reads computed styles to detect CSS animations - Listens to `animationend`, `animationcancel`, and `animationstart` events - Waits for animations to complete before unmounting - Handles animation interruptions (e.g., when `present` changes mid-animation) ... ## Important Notes ... The component detects CSS animations by reading the `animation-name` computed style. Make sure your exit animations have a different `animation-name` than entry animations. ... If the element has `display: none` or no animation defined, it will unmount immediately when `present` becomes `false`. ... The component uses `useLayoutEffect` to synchronously detect animation changes before the browser paints, preventing flashing. ... When using the render function pattern, the `present` prop lets you conditionally render content or apply different styles during entry and exit. ... 1. Always define exit animations - Without them, the component unmounts immediately 2. Use different animation names - Entry and exit animations should have distinct names 3. Set animation-fill-mode - The component sets `forwards` during exit to prevent flashing 4. Test animation timing - Ensure animations complete before state changes occur ... component relies on: ... - CSS Animations API - `getComputedStyle()` - Animation events (`animationend`, `animationcancel`, `animationstart`) <title>Result 4</title> https://www.radix-ui.com/primitives/docs/guides/animation # Animation Animate Radix Primitives with CSS keyframes or the JavaScript animation library of your choice. Adding animation to Radix Primitives should feel similar to any other component, but there are some caveats noted here in regards to exiting animations with JS animation libraries. ## Animating with CSS animation The simplest way to animate Primitives is with CSS. You can use CSS animation to animate both mount and unmount phases. The latter is possible because the Radix Primitives will suspend unmount while your animation plays out. ```css `@keyframes` fadeIn { from { opacity: 0; } to { opacity: 1; } } `@keyframes` fadeOut { from { opacity: 1; } to { opacity: 0; } } .DialogOverlay[data-state="open"], .DialogContent[data-state="open"] { animation: fadeIn 300ms ease-out; } .DialogOverlay[data-state="closed"], .DialogContent[data-state="closed"] { animation: fadeOut 300ms ease-in; } ``` ## Delegating unmounting for JavaScript Animation When many stateful Primitives are hidden from view, they are actually removed from the React Tree, and their elements removed from the DOM. JavaScript animation libraries need control of the unmounting phase, so we provide the `forceMount` prop on many components to allow consumers to delegate the mounting and unmounting of children based on the animation state determined by those libraries. For example, if you want to use React Spring to animate a `Dialog`, you would do so by conditionally rendering the dialog `Overlay` and `Content` parts based on the animation state from one of its hooks like `useTransition`: ```jsx import { Dialog } from "radix-ui"; import { useTransition, animated, config } from "react-spring"; function Example() { const [open, setOpen] = React.useState(false); const transitions = useTransition(open, { from: { opacity: 0, y: -10 }, enter: { opacity: 1, y: 0 }, leave: { opacity: 0, y: 10 }, config: config.stiff, }); return ( <Dialog.Root open={open} onOpenChange={setOpen}> <Dialog.Trigger>Open Dialog</Dialog.Trigger> {transitions((styles, item) => item ? ( <> <Dialog.Overlay forceMount asChild> <animated.div style={{ opacity: styles.opacity, }} /> </Dialog.Overlay> <Dialog.Content forceMount asChild> <animated.div style={styles}> <h1>Hello from inside the Dialog!</h1> <Dialog.Close>close</Dialog.Close> </animated.div> </Dialog.Content> </> ) : null, )} </Dialog.Root> ); } ``` <title>Result 5</title> https://www.radix-ui.com/primitives/docs/components/dialog # Dialog A window overlaid on either the primary window or another dialog window, rendering the content underneath inert. ## Features - Supports modal and non-modal modes. - Focus is automatically trapped within modal. - Can be controlled or uncontrolled. - Manages screen reader announcements with `Title` and `Description` components. - Esc closes the component automatically. ## Anatomy Import all parts and piece them together. ```jsx import { Dialog } from "radix-ui"; export default () => ( <Dialog.Root> <Dialog.Trigger /> <Dialog.Portal> <Dialog.Overlay /> <Dialog.Content> <Dialog.Title /> <Dialog.Description /> <Dialog.Close /> </Dialog.Content> </Dialog.Portal> </Dialog.Root> ); ``` ## API Reference ### Root Contains all the parts of a dialog. | Prop | Type | Default | | --- | --- | --- | | `defaultOpen` | `boolean` | No default value | | `open` | `boolean` | No default value | | `onOpenChange` | `function` | No default value | | `modal` | `boolean` | `true` | ### Trigger The button that opens the dialog. | Prop | Type | Default | | --- | --- | --- | | `asChild` | `boolean` | `false` | | Data attribute | Values | | --- | --- | | `[data-state]` | `"open" | "closed"` | ### Portal When used, portals your overlay and content parts into the `body`. | Prop | Type | Default | | --- | --- | --- | | `forceMount` | `boolean` | No default value | | `container` | `HTMLElement` | `document.body` | ### Overlay A layer that covers the inert portion of the view when the dialog is open. | Prop | Type | Default | | --- | --- | --- | | `asChild` | `boolean` | `false` | | `forceMount` | `boolean` | No default value | | Data attribute | Values | | --- | --- | | `[data-state]` | `"open" | "closed"` | ### Content Contains content to be rendered in the open dialog. | Prop | Type | Default | | --- | --- | --- | | `asChild` | `boolean` | `false` | | `forceMount` | `boolean` | No default value | | `onOpenAutoFocus` | `function` | No default value | | `onCloseAutoFocus` | `function` | No default value | | `onEscapeKeyDown` | `function` | No default value | | `onPointerDownOutside` | `function` | No default value | | `onInteractOutside` | `function` | No default value | | Data attribute | Values | | --- | --- | | `[data-state]` | `"open" | "closed"` | ### Close The button that closes the dialog. | Prop | Type | Default | | --- | --- | --- | | `asChild` | `boolean` | `false` | ### Title An accessible title to be announced when the dialog is opened. If you want to hide the title, wrap it inside our Visually Hidden utility like this ``. | Prop | Type | Default | | --- | --- | --- | | `asChild` | `boolean` | `false` | ### Description An optional accessible description to be announced when the dialog is opened. If you want to hide the description, wrap it inside our Visually Hidden utility like this ``. If you want to remove the description entirely, remove this part and pass `aria-describedby={undefined}` to `Dialog.Content`. | Prop | Type | Default | | --- | --- | --- | | `asChild` | `boolean` | `false` | ## Examples ### Close after asynchronous form submission Use the controlled props to programmatically close the Dialog after an async operation has completed. ```jsx import * as React from "react"; import { Dialog } from "radix-ui"; const wait = () => new Promise((resolve) => setTimeout(resolve, 1000)); export default () => { const [open, setOpen] = React.useState(false); return ( <Dialog.Root open={open} onOpenChange={setOpen}> <Dialog.Trigger>Open</Dialog.Trigger> <Dialog.Portal> <Dialog.Overlay /> <Dialog.Content> <form onSubmit={(event) => { wait().then(() => setOpen(false)); event.preventDefault(); }} > {/** some inputs */} <button type="submit">Submit</button> </form> </Dialog.Content> <…[truncated]

Citations:


Keep DialogContent mounted during close.

DialogContent wraps radix-ui@1.6.2's Dialog.Content, which uses Presence to wait for data-state="closed" animations. The {open ? ... : null} branch removes the content before Presence can run, so the fade/zoom-out animation is skipped. Render DialogContent unconditionally and let Dialog control its presence.

Comment on lines +4435 to +4439
"1b0a156717": "Agents",
"2ec931a922": "Close agent session?",
"e1250e23e9": "This closes the entire terminal tab running this agent, including all of its split panes. Any other agents running in those panes will be stopped too.",
"7d17689ff0": "Cancel",
"6ac88e0ff9": "Close session"

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

Translate the new values in es.json.

These entries remain in English in the Spanish locale. Spanish users will see English labels and close-session confirmation text in the sidebar, dashboard, and shared dialog. Replace them with Spanish translations in all three blocks.

Proposed translations
- "1b0a156717": "Agents",
+ "1b0a156717": "Agentes",
- "2ec931a922": "Close agent session?",
+ "2ec931a922": "¿Cerrar la sesión del agente?",
- "e1250e23e9": "This closes the entire terminal tab running this agent, including all of its split panes. Any other agents running in those panes will be stopped too.",
+ "e1250e23e9": "Esto cierra la pestaña de terminal completa donde se ejecuta este agente, incluidos todos sus paneles divididos. También se detendrán los demás agentes que se ejecuten en esos paneles.",
- "7d17689ff0": "Cancel",
+ "7d17689ff0": "Cancelar",
- "6ac88e0ff9": "Close session",
+ "6ac88e0ff9": "Cerrar sesión",
- "271b256627": "Close agent session",
+ "271b256627": "Cerrar la sesión del agente",
- "1c27be5226": "Close session"
+ "1c27be5226": "Cerrar sesión"
- "closeTitle": "Close agent session?",
+ "closeTitle": "¿Cerrar la sesión del agente?",
- "closeDescription": "This closes the entire terminal tab running this agent, including all of its split panes. Any other agents running in those panes will be stopped too.",
+ "closeDescription": "Esto cierra la pestaña de terminal completa donde se ejecuta este agente, incluidos todos sus paneles divididos. También se detendrán los demás agentes que se ejecuten en esos paneles.",
- "closeCancel": "Cancel",
+ "closeCancel": "Cancelar",
- "closeConfirm": "Close session",
+ "closeConfirm": "Cerrar sesión",
- "closeSession": "Close session"
+ "closeSession": "Cerrar sesión"

Also applies to: 13206-13207, 14533-14537

Comment on lines +4417 to +4420
"2ec931a922": "Close agent session?",
"e1250e23e9": "This closes the entire terminal tab running this agent, including all of its split panes. Any other agents running in those panes will be stopped too.",
"7d17689ff0": "Cancel",
"6ac88e0ff9": "Close session"

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

Translate the new close-session strings.

The Japanese locale contains English values for the close-session dialogs and controls. Japanese users will see English text in the sidebar, dashboard, and popout flows.

Add Japanese translations to all three namespaces. Keep repeated labels such as closeConfirm and closeSession consistent.

This is required by the PR objective to localize the close flow in Japanese.

Also applies to: 13206-13207, 14533-14537

Comment on lines +4417 to +4420
"2ec931a922": "Close agent session?",
"e1250e23e9": "This closes the entire terminal tab running this agent, including all of its split panes. Any other agents running in those panes will be stopped too.",
"7d17689ff0": "Cancel",
"6ac88e0ff9": "Close session"

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

Add Korean translations for the new close-session strings.

The values at Lines 4417-4420, Lines 13206-13207, and Lines 14533-14537 are still English. Korean users will see English labels and confirmation text in the sidebar, dashboard, and popout flows. Replace these values with approved Korean translations while preserving the existing message meaning.

Also applies to: 13206-13207, 14533-14537

Comment on lines +4417 to +4420
"2ec931a922": "Close agent session?",
"e1250e23e9": "This closes the entire terminal tab running this agent, including all of its split panes. Any other agents running in those panes will be stopped too.",
"7d17689ff0": "Cancel",
"6ac88e0ff9": "Close session"

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

Do not add English placeholders to zh.json.

The new close-session strings override the fallback and display English in the Simplified Chinese UI. Translate each key, or omit each key until the translation is available.

  • src/renderer/src/i18n/locales/zh.json#L4417-L4420: translate the WorktreeCardAgents confirmation strings.
  • src/renderer/src/i18n/locales/zh.json#L13206-L13207: translate the DashboardAgentRow close-session labels.
  • src/renderer/src/i18n/locales/zh.json#L14533-L14537: translate the dashboardPopout confirmation strings.

Based on learnings: new English-source keys should remain absent from non-English locale files until real translations exist.

📍 Affects 1 file
  • src/renderer/src/i18n/locales/zh.json#L4417-L4420 (this comment)
  • src/renderer/src/i18n/locales/zh.json#L13206-L13207
  • src/renderer/src/i18n/locales/zh.json#L14533-L14537

Source: Learnings

@MumuTW
MumuTW deleted the MumuTW/close-agent-from-dashboard branch September 12, 2026 12:55
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