feat(desktop): restart daemon from terminal-unreachable toast#4962
feat(desktop): restart daemon from terminal-unreachable toast#4962Kitenite wants to merge 1 commit into
Conversation
After auto-reconnect exhausts its 5 retries, surface a persistent toast with a "Restart daemon" action so users can recover without digging into Settings → Manage sessions.
|
Capy auto-review is paused for this organization because the monthly auto-review limit has been reached. Increase the limit or turn it off in billing settings to resume automatic reviews. |
|
Ready to review this PR? Stage has broken it down into 1 individual chapter for you:
Chapters generated by Stage for commit 51fdacf on May 28, 2026 12:08am UTC. |
📝 WalkthroughWalkthroughTerminal.tsx now imports ChangesTerminal daemon restart UI and notifications
Sequence DiagramsequenceDiagram
participant RetryEffect as Connection Retry Effect
participant Toast as Toast Service
participant User as User
participant Mutation as Restart Daemon Mutation
RetryEffect->>RetryEffect: Check max retries exceeded
RetryEffect->>Toast: Show "unreachable" error toast
Toast->>User: Display notification with action
User->>Toast: Click restart action
Toast->>Mutation: Trigger restart daemon
Mutation->>Toast: Show success/error toast
Toast->>User: Display result
Estimated code review effort🎯 2 (Simple) | ⏱️ ~10 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add 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 |
Greptile SummaryThis PR adds a persistent "Terminal daemon unreachable" toast that fires once after the 5-retry auto-reconnect cycle is exhausted, with a "Restart daemon" inline action that calls the same
Confidence Score: 3/5The core reconnect logic and guard flag are sound, but a persistent infinite-duration toast is shown without any cleanup when the terminal pane is closed, leaving a stale "Restart daemon" button visible and actionable after the originating component is gone. The Terminal.tsx — specifically the toast creation block and the absence of a cleanup effect to dismiss the persistent toast on unmount.
|
| Filename | Overview |
|---|---|
| apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx | Adds a persistent Sonner toast for daemon unreachability with a "Restart daemon" action after 5 failed reconnect attempts; missing toast cleanup on unmount allows a stale toast to persist after the pane is closed |
Sequence Diagram
sequenceDiagram
participant T as Terminal Component
participant E as useEffect (connectionError)
participant S as Sonner Toast
participant R as restartDaemon tRPC
T->>E: connectionError set
E->>E: "retryCountRef < MAX_RETRIES?"
E->>T: setTimeout(handleRetryConnection, delay)
T->>E: connectionError still set (retry fails)
note over E: retryCountRef reaches MAX_RETRIES
E->>S: "toast.error(duration=∞)"
note over S: daemonFailureToastShownRef = true
S-->>T: User clicks Restart daemon
T->>R: restartDaemonMutationRef.current.mutate()
R-->>S: onSuccess → toast.success
T->>E: connectionError clears (data event)
E->>E: "daemonFailureToastShownRef = false"
note over S: Toast NOT dismissed on pane unmount
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx:299-315
**Persistent toast not dismissed on component unmount**
The toast is created with `duration: Number.POSITIVE_INFINITY` but no cleanup runs when this `Terminal` instance unmounts (e.g., user closes the pane). The toast stays visible and functional indefinitely. If the user then clicks "Restart daemon," the mutation fires from an unmounted component — the daemon restarts, but `onSuccess`/`onError` can run against a detached component lifecycle. The existing `useUpdateListener` in this codebase shows the correct pattern: assign a stable `id` to the toast and call `toast.dismiss(id)` in a `useEffect` cleanup. For example, store the returned toast ID in a ref (`const toastIdRef = useRef<string | number | null>(null)`) and add a `useEffect(() => () => { if (toastIdRef.current != null) toast.dismiss(toastIdRef.current); }, [])` to guarantee cleanup on unmount.
### Issue 2 of 2
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx:303-313
**Multiple failing panes accumulate persistent toasts**
Because each `Terminal` instance manages its own `daemonFailureToastShownRef` and shows the toast independently, opening 3 terminal panes when the daemon is unreachable results in 3 stacked "Terminal daemon unreachable" toasts, each with its own "Restart daemon" button. Since this is a daemon-level (not pane-level) failure, a shared toast ID (e.g., a module-level constant like `DAEMON_TOAST_ID`) would allow Sonner to deduplicate updates to the same toast rather than stacking them. This mirrors how `useUpdateListener` uses `UPDATE_TOAST_ID` to prevent duplicate update toasts.
Reviews (1): Last reviewed commit: "feat(desktop): offer daemon restart from..." | Re-trigger Greptile
| if (isExitedRef.current) return; | ||
| if (retryCountRef.current >= MAX_RETRIES) return; | ||
| if (retryCountRef.current >= MAX_RETRIES) { | ||
| if (!daemonFailureToastShownRef.current) { | ||
| daemonFailureToastShownRef.current = true; | ||
| toast.error("Terminal daemon unreachable", { | ||
| description: | ||
| "Couldn't reconnect after several attempts. Restart the daemon to recover.", | ||
| duration: Number.POSITIVE_INFINITY, | ||
| action: { | ||
| label: "Restart daemon", | ||
| onClick: () => { | ||
| restartDaemonMutationRef.current.mutate(); | ||
| }, | ||
| }, | ||
| }); | ||
| } | ||
| return; |
There was a problem hiding this comment.
Persistent toast not dismissed on component unmount
The toast is created with duration: Number.POSITIVE_INFINITY but no cleanup runs when this Terminal instance unmounts (e.g., user closes the pane). The toast stays visible and functional indefinitely. If the user then clicks "Restart daemon," the mutation fires from an unmounted component — the daemon restarts, but onSuccess/onError can run against a detached component lifecycle. The existing useUpdateListener in this codebase shows the correct pattern: assign a stable id to the toast and call toast.dismiss(id) in a useEffect cleanup. For example, store the returned toast ID in a ref (const toastIdRef = useRef<string | number | null>(null)) and add a useEffect(() => () => { if (toastIdRef.current != null) toast.dismiss(toastIdRef.current); }, []) to guarantee cleanup on unmount.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
Line: 299-315
Comment:
**Persistent toast not dismissed on component unmount**
The toast is created with `duration: Number.POSITIVE_INFINITY` but no cleanup runs when this `Terminal` instance unmounts (e.g., user closes the pane). The toast stays visible and functional indefinitely. If the user then clicks "Restart daemon," the mutation fires from an unmounted component — the daemon restarts, but `onSuccess`/`onError` can run against a detached component lifecycle. The existing `useUpdateListener` in this codebase shows the correct pattern: assign a stable `id` to the toast and call `toast.dismiss(id)` in a `useEffect` cleanup. For example, store the returned toast ID in a ref (`const toastIdRef = useRef<string | number | null>(null)`) and add a `useEffect(() => () => { if (toastIdRef.current != null) toast.dismiss(toastIdRef.current); }, [])` to guarantee cleanup on unmount.
How can I resolve this? If you propose a fix, please make it concise.| toast.error("Terminal daemon unreachable", { | ||
| description: | ||
| "Couldn't reconnect after several attempts. Restart the daemon to recover.", | ||
| duration: Number.POSITIVE_INFINITY, | ||
| action: { | ||
| label: "Restart daemon", | ||
| onClick: () => { | ||
| restartDaemonMutationRef.current.mutate(); | ||
| }, | ||
| }, | ||
| }); |
There was a problem hiding this comment.
Multiple failing panes accumulate persistent toasts
Because each Terminal instance manages its own daemonFailureToastShownRef and shows the toast independently, opening 3 terminal panes when the daemon is unreachable results in 3 stacked "Terminal daemon unreachable" toasts, each with its own "Restart daemon" button. Since this is a daemon-level (not pane-level) failure, a shared toast ID (e.g., a module-level constant like DAEMON_TOAST_ID) would allow Sonner to deduplicate updates to the same toast rather than stacking them. This mirrors how useUpdateListener uses UPDATE_TOAST_ID to prevent duplicate update toasts.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
Line: 303-313
Comment:
**Multiple failing panes accumulate persistent toasts**
Because each `Terminal` instance manages its own `daemonFailureToastShownRef` and shows the toast independently, opening 3 terminal panes when the daemon is unreachable results in 3 stacked "Terminal daemon unreachable" toasts, each with its own "Restart daemon" button. Since this is a daemon-level (not pane-level) failure, a shared toast ID (e.g., a module-level constant like `DAEMON_TOAST_ID`) would allow Sonner to deduplicate updates to the same toast rather than stacking them. This mirrors how `useUpdateListener` uses `UPDATE_TOAST_ID` to prevent duplicate update toasts.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx`:
- Around line 309-311: Guard the mutation call in the onClick handler by
checking the mutation's pending/loading state before invoking mutate: in
Terminal.tsx locate the onClick that calls
restartDaemonMutationRef.current.mutate() and change it to first read the ref
(e.g. const m = restartDaemonMutationRef.current) and return early if
m?.isPending || m?.isLoading (or the appropriate flag your mutation exposes);
only call m.mutate() (or m.mutateAsync()) when not pending. This prevents
concurrent restarts while keeping the existing restartDaemonMutationRef and
mutate call intact.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 9249ff3f-11ea-4c8b-902a-ab0235bba249
📒 Files selected for processing (1)
apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx
| onClick: () => { | ||
| restartDaemonMutationRef.current.mutate(); | ||
| }, |
There was a problem hiding this comment.
Guard against concurrent mutation calls.
The action button can be clicked multiple times before the mutation completes, potentially triggering concurrent daemon restarts. Add an isPending check to prevent this.
🛡️ Proposed fix to add pending state guard
action: {
label: "Restart daemon",
onClick: () => {
+ if (!restartDaemonMutationRef.current.isPending) {
restartDaemonMutationRef.current.mutate();
+ }
},
},📝 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.
| onClick: () => { | |
| restartDaemonMutationRef.current.mutate(); | |
| }, | |
| onClick: () => { | |
| if (!restartDaemonMutationRef.current.isPending) { | |
| restartDaemonMutationRef.current.mutate(); | |
| } | |
| }, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx`
around lines 309 - 311, Guard the mutation call in the onClick handler by
checking the mutation's pending/loading state before invoking mutate: in
Terminal.tsx locate the onClick that calls
restartDaemonMutationRef.current.mutate() and change it to first read the ref
(e.g. const m = restartDaemonMutationRef.current) and return early if
m?.isPending || m?.isLoading (or the appropriate flag your mutation exposes);
only call m.mutate() (or m.mutateAsync()) when not pending. This prevents
concurrent restarts while keeping the existing restartDaemonMutationRef and
mutate call intact.
There was a problem hiding this comment.
3 issues found across 1 file
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx">
<violation number="1" location="apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx:296">
P2: The persistent "Terminal daemon unreachable" toast is never dismissed on reconnect, so a stale error can remain visible after recovery.</violation>
<violation number="2" location="apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx:303">
P2: Multiple terminal panes will each show their own "Terminal daemon unreachable" toast since `daemonFailureToastShownRef` is per-instance and no shared `id` is passed to `toast.error()`. Use a module-level constant toast ID (e.g., `const DAEMON_UNREACHABLE_TOAST_ID = "daemon-unreachable"`) so Sonner deduplicates rather than stacking identical toasts.</violation>
<violation number="3" location="apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx:306">
P2: The infinite-duration toast is never dismissed when this component unmounts (e.g., user closes the terminal pane). The toast remains visible and functional with a stale ref. Assign a stable `id` to the toast and dismiss it in a `useEffect` cleanup to prevent interaction with an unmounted component's state.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Terminal as Terminal Component
participant Retry as Auto-Reconnect Logic
participant Toast as Toast System
participant Mutation as restartDaemon Mutation
participant Backend as Terminal Daemon (pty-host)
Note over Terminal,Backend: Terminal Daemon Unreachable Flow
Terminal->>Retry: Connection error detected
Retry->>Retry: retryCountRef >= MAX_RETRIES (5)?
alt Connection error cleared
Retry->>Terminal: Reset daemonFailureToastShownRef
else Retries exhausted
Retry->>Retry: daemonFailureToastShownRef is false?
alt Toast not yet shown
Retry->>Toast: Show "Terminal daemon unreachable" toast (persistent)
Toast->>Toast: Set daemonFailureToastShownRef = true
end
Note over Terminal,Toast: User clicks "Restart daemon"
Toast->>Mutation: trigger restartDaemon mutation
Mutation->>Backend: electronTrpc.terminal.restartDaemon
Backend-->>Mutation: Restart result
alt Success
Mutation->>Toast: Show success toast "Daemon restarted"
else Error
Mutation->>Toast: Show error toast with error.message
end
end
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| toast.error("Terminal daemon unreachable", { | ||
| description: | ||
| "Couldn't reconnect after several attempts. Restart the daemon to recover.", | ||
| duration: Number.POSITIVE_INFINITY, |
There was a problem hiding this comment.
P2: The infinite-duration toast is never dismissed when this component unmounts (e.g., user closes the terminal pane). The toast remains visible and functional with a stale ref. Assign a stable id to the toast and dismiss it in a useEffect cleanup to prevent interaction with an unmounted component's state.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx, line 306:
<comment>The infinite-duration toast is never dismissed when this component unmounts (e.g., user closes the terminal pane). The toast remains visible and functional with a stale ref. Assign a stable `id` to the toast and dismiss it in a `useEffect` cleanup to prevent interaction with an unmounted component's state.</comment>
<file context>
@@ -272,9 +292,28 @@ export const Terminal = memo(function Terminal({
+ toast.error("Terminal daemon unreachable", {
+ description:
+ "Couldn't reconnect after several attempts. Restart the daemon to recover.",
+ duration: Number.POSITIVE_INFINITY,
+ action: {
+ label: "Restart daemon",
</file context>
| if (retryCountRef.current >= MAX_RETRIES) { | ||
| if (!daemonFailureToastShownRef.current) { | ||
| daemonFailureToastShownRef.current = true; | ||
| toast.error("Terminal daemon unreachable", { |
There was a problem hiding this comment.
P2: Multiple terminal panes will each show their own "Terminal daemon unreachable" toast since daemonFailureToastShownRef is per-instance and no shared id is passed to toast.error(). Use a module-level constant toast ID (e.g., const DAEMON_UNREACHABLE_TOAST_ID = "daemon-unreachable") so Sonner deduplicates rather than stacking identical toasts.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx, line 303:
<comment>Multiple terminal panes will each show their own "Terminal daemon unreachable" toast since `daemonFailureToastShownRef` is per-instance and no shared `id` is passed to `toast.error()`. Use a module-level constant toast ID (e.g., `const DAEMON_UNREACHABLE_TOAST_ID = "daemon-unreachable"`) so Sonner deduplicates rather than stacking identical toasts.</comment>
<file context>
@@ -272,9 +292,28 @@ export const Terminal = memo(function Terminal({
+ if (retryCountRef.current >= MAX_RETRIES) {
+ if (!daemonFailureToastShownRef.current) {
+ daemonFailureToastShownRef.current = true;
+ toast.error("Terminal daemon unreachable", {
+ description:
+ "Couldn't reconnect after several attempts. Restart the daemon to recover.",
</file context>
| toast.error("Terminal daemon unreachable", { | |
| toast.error("Terminal daemon unreachable", { | |
| id: "daemon-unreachable", |
| useEffect(() => { | ||
| if (!connectionError) return; | ||
| if (!connectionError) { | ||
| daemonFailureToastShownRef.current = false; |
There was a problem hiding this comment.
P2: The persistent "Terminal daemon unreachable" toast is never dismissed on reconnect, so a stale error can remain visible after recovery.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/src/renderer/screens/main/components/WorkspaceView/ContentView/TabsContent/Terminal/Terminal.tsx, line 296:
<comment>The persistent "Terminal daemon unreachable" toast is never dismissed on reconnect, so a stale error can remain visible after recovery.</comment>
<file context>
@@ -272,9 +292,28 @@ export const Terminal = memo(function Terminal({
useEffect(() => {
- if (!connectionError) return;
+ if (!connectionError) {
+ daemonFailureToastShownRef.current = false;
+ return;
+ }
</file context>
Summary
electronTrpc.terminal.restartDaemonmutation used by Settings → Terminal → Manage sessions.Test plan
Summary by cubic
Adds a persistent "Terminal daemon unreachable" toast after 5 failed auto-reconnect attempts, with a Restart daemon action that calls
electronTrpc.terminal.restartDaemon. The toast shows only once per failure, resets on recovery, and displays success/error feedback.Written for commit 51fdacf. Summary will update on new commits.
Review in cubic
Summary by CodeRabbit
Release Notes