Skip to content

fix: preserve live terminal sessions#65

Merged
gnoviawan merged 6 commits intodevfrom
fix/preserve-live-terminal-sessions
Mar 9, 2026
Merged

fix: preserve live terminal sessions#65
gnoviawan merged 6 commits intodevfrom
fix/preserve-live-terminal-sessions

Conversation

@gnoviawan
Copy link
Owner

@gnoviawan gnoviawan commented Mar 9, 2026

Summary

  • preserve live PTY-backed terminals across project switches instead of tearing them down
  • restore project-specific workspace layouts while remapping live terminal tabs and guarding against stale async restores
  • add regression coverage for workspace tab focus, terminal recovery, and rapid project switching

Test plan

  • npm test -- --run src/renderer/hooks/use-editor-persistence.test.ts
  • npm test -- --run src/renderer/stores/workspace-store.test.ts
  • npm test -- --run src/renderer/hooks/use-terminal-restore.test.ts

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Terminal persistence with remapping: persisted terminal tabs are reconciled with live terminals on restore.
    • Workspace APIs to load a project layout and ensure terminal tabs exist.
  • Improvements

    • Per-project restore tracking and safe cancellation to avoid applying stale restores.
    • Preservation and fallback behaviors when switching projects, handling legacy entries, and resetting when no persisted state.
    • More robust terminal-tab synchronization during layout updates.
  • Tests

    • Expanded coverage for terminal restore, remapping, cancellation, and workspace restoration flows.

Keep live PTY-backed terminals and project workspace layouts stable when switching projects, while guarding restore flows against stale async updates and focus drift.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds terminal-aware editor persistence and restoration: per-restore cancellation/owner tracking, reconciliation between persisted terminal tabs and live PTYs, workspace-store APIs (loadProjectWorkspace, ensureTerminalTab), and expanded tests and mocks exercising remapping, stale-restore handling, and multi-project flows.

Changes

Cohort / File(s) Summary
Editor persistence & tests
src/renderer/hooks/use-editor-persistence.ts, src/renderer/hooks/use-editor-persistence.test.ts
Integrates terminal persistence into editor restore flow: tracks restoreRunId/cancellation, loads persisted terminals, reconciles persisted vs live terminals via new helpers, and calls loadProjectWorkspace; tests updated to use hoisted loadProjectWorkspace and terminal mocks.
Terminal restore & tests
src/renderer/hooks/use-terminal-restore.ts, src/renderer/hooks/use-terminal-restore.test.ts
Makes terminal restore cancellation-aware and per-restore owner-tracked, threads isCancelled checks through spawn/restore flows, remaps old->new terminal IDs, cleans up spawned PTYs on cancel/error, and adds extensive tests for spawn/cancel/project-switch scenarios.
Terminal autosave & tests
src/renderer/hooks/useTerminalAutoSave.ts, src/renderer/hooks/useTerminalAutoSave.test.ts
Replaces single-flag tracking with per-project owner map; setTerminalRestoreInProgress(projectId, isRestoring, ownerId) and isTerminalRestoreInProgress() added and tested to gate auto-save during restores.
Workspace store & tests
src/renderer/stores/workspace-store.ts, src/renderer/stores/workspace-store.test.ts
Adds loadProjectWorkspace(root, activePaneId?) to normalize/replace pane tree and resolve active pane, and ensureTerminalTab(terminalId, targetPaneId?, makeActive?) to create/activate terminal tabs; tests verifying ensure behavior added.
Workspace layout
src/renderer/layouts/WorkspaceLayout.tsx
Replaces sync logic with idempotent ensure loop: tracks lastEnsuredTerminalIds and calls ensureTerminalTab per terminal ID to avoid redundant syncs.
Misc mocks & test plumbing
src/renderer/hooks/*test.ts (various), vi.hoisted mocks
Adds hoisted/mock scaffolding for loadPersistedTerminals, terminal store state, and wires terminal auto-save/load mocks into tests; updates many tests to assert via loadProjectWorkspace calls.

Sequence Diagram

sequenceDiagram
    participant Editor as Editor Persistence
    participant TerminalRestore as Terminal Restore
    participant TerminalStore as Terminal Store
    participant WorkspaceStore as Workspace Store
    participant AutoSave as Terminal Auto-Save

    Editor->>TerminalRestore: restore(projectId)
    activate TerminalRestore
    TerminalRestore->>AutoSave: setTerminalRestoreInProgress(projectId, true, ownerId)
    TerminalRestore->>TerminalStore: loadPersistedTerminals(projectId)
    TerminalRestore->>TerminalStore: fetch live terminals for project
    TerminalRestore->>TerminalRestore: isCancelled? (abort if true)
    TerminalRestore->>TerminalRestore: reconcile persisted layout with live terminals -> mapping
    TerminalRestore->>WorkspaceStore: ensureTerminalTab(mapped/live terminalIds...)
    TerminalRestore->>TerminalRestore: isCancelled? (abort if true)
    alt Not cancelled
        TerminalRestore->>WorkspaceStore: loadProjectWorkspace(reconciledRoot, activePaneId)
    end
    TerminalRestore->>AutoSave: setTerminalRestoreInProgress(projectId, false, ownerId)
    deactivate TerminalRestore
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • gnoviawan/termul PR 25: Overlaps workspace persistence and terminal-restore logic (layout reconciliation and restore flow changes).
  • gnoviawan/termul PR 20: Modifies similar editor/workspace persistence code and workspace-store APIs used here.
  • gnoviawan/termul PR 60: Related changes to terminal spawn/restore behavior and shell resolution during terminal creation.

Poem

🐰
I hopped through panes and tabs tonight,
I matched old PTYs to terminals bright,
when projects switch and layouts roam,
I stitch the tabs and guide them home,
a rabbit's paw keeps states polite.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: preserve live terminal sessions' directly and clearly summarizes the primary change across the pull request files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/preserve-live-terminal-sessions

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 and usage tips.

@gnoviawan gnoviawan marked this pull request as ready for review March 9, 2026 04:18
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/renderer/hooks/use-terminal-restore.ts (1)

369-409: Consider removing unused _projectId parameter.

The selectTerminalForProject function no longer uses the projectId parameter (renamed to _projectId to suppress the lint warning). Since the function now just returns a terminal ID based on existingTerminals and layout, this parameter could be removed entirely.

♻️ Optional cleanup
 function selectTerminalForProject(
-  _projectId: string,
   existingTerminals: Array<{ id: string; name: string; projectId: string }>,
   layout: PersistedTerminalLayout | null
 ): string | null {

And update the call site at line 272-276:

           const terminalIdToSelect = selectTerminalForProject(
-            projectIdToRestore,
             liveProjectTerminals,
             layout
           )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/use-terminal-restore.ts` around lines 369 - 409, Remove
the unused _projectId parameter from the selectTerminalForProject function
signature and its internal references so the function only accepts
(existingTerminals, layout) and returns the selected terminal ID; then update
all call sites that pass a projectId (notably the invocation in the same file
that currently supplies a projectId argument) to call
selectTerminalForProject(existingTerminals, layout) instead, and run typechecks
to update any TypeScript signatures/usages accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/hooks/use-editor-persistence.test.ts`:
- Around line 494-556: The test declares resolveProjectA as "((value: ...) =>
void) | null" so TypeScript disallows calling it via optional chaining; change
its declared union to use undefined instead of null (i.e., "... | undefined") OR
replace the optional-chained call site "resolveProjectA?.(...)" with an explicit
runtime check and call (if (resolveProjectA) resolveProjectA(...)); update the
declaration for resolveProjectA and the call in the 'ignores stale restore
results after switching projects' test that uses useEditorPersistence to match
one of these fixes.

In `@src/renderer/hooks/use-terminal-restore.test.ts`:
- Around line 145-184: Change the resolver variable declaration so TypeScript
knows it may be undefined (not null) — replace "let resolveProjectALayout:
((value: null) => void) | null = null" with "let resolveProjectALayout: ((value:
null) => void) | undefined = undefined" (so resolveProjectALayout?.(null) is
seen as callable), leaving the rest of the test (the Promise executor assigning
it and the resolveProjectALayout?.(null) call) unchanged.
- Around line 186-212: The test's call to rejectProjectALayout is causing a
TypeScript "not callable" error; change the invocation to an explicit null check
before calling (e.g. if (rejectProjectALayout) rejectProjectALayout(new
Error('restore failed'))), updating the line that currently does
rejectProjectALayout?.(new Error('restore failed')) so the variable declared as
let rejectProjectALayout: ((reason?: unknown) => void) | null = null is narrowed
before use.

---

Nitpick comments:
In `@src/renderer/hooks/use-terminal-restore.ts`:
- Around line 369-409: Remove the unused _projectId parameter from the
selectTerminalForProject function signature and its internal references so the
function only accepts (existingTerminals, layout) and returns the selected
terminal ID; then update all call sites that pass a projectId (notably the
invocation in the same file that currently supplies a projectId argument) to
call selectTerminalForProject(existingTerminals, layout) instead, and run
typechecks to update any TypeScript signatures/usages accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a0a9bf90-d6d1-4076-96bc-8442d306d0d5

📥 Commits

Reviewing files that changed from the base of the PR and between 5f0bc22 and 7784a08.

📒 Files selected for processing (8)
  • src/renderer/hooks/use-editor-persistence.test.ts
  • src/renderer/hooks/use-editor-persistence.ts
  • src/renderer/hooks/use-terminal-restore.test.ts
  • src/renderer/hooks/use-terminal-restore.ts
  • src/renderer/hooks/useTerminalAutoSave.ts
  • src/renderer/layouts/WorkspaceLayout.tsx
  • src/renderer/stores/workspace-store.test.ts
  • src/renderer/stores/workspace-store.ts

Tighten the follow-up test typings and remove the unused project parameter from terminal restore selection to keep the PR green under typecheck.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/hooks/use-terminal-restore.ts (1)

298-307: ⚠️ Potential issue | 🔴 Critical

Cancellation stops before the expensive restore work actually finishes.

After the Line 299 guard, the code still awaits restoreFromLayout() / createDefaultTerminal(). Both helpers do more async work and then mutate the stores without rechecking cancellation, so a fast project switch here can still spawn PTYs or re-add terminals for a stale project.

🐛 Proposed fix
-        if (layout && layout.terminals.length > 0) {
-          await restoreFromLayout(projectIdToRestore, layout)
-        } else {
-          await createDefaultTerminal(projectIdToRestore)
+        if (layout && layout.terminals.length > 0) {
+          await restoreFromLayout(projectIdToRestore, layout, isCancelled)
+        } else {
+          await createDefaultTerminal(projectIdToRestore, isCancelled)
         }

Then bail out inside those helpers before each store mutation and after awaited spawn steps.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/use-terminal-restore.ts` around lines 298 - 307, The
restore flow in useTerminalRestore still awaits
restoreFromLayout(projectIdToRestore, layout) and
createDefaultTerminal(projectIdToRestore) even after the isCancelled() guard, so
those helpers can continue async work and mutate stores for a stale project;
update restoreFromLayout and createDefaultTerminal to check cancellation (use
the same isCancelled or callId guard) before every store mutation and
immediately after any awaited spawn/long async step, returning early if
cancelled to prevent spawning PTYs or re-adding terminals for the wrong project
in useTerminalRestore.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/hooks/use-terminal-restore.test.ts`:
- Around line 186-211: The test can spuriously pass because it only waits for
mockSetTerminalRestoreInProgress('project-b', true) and not for the
cancelled-restore path to finish, so ensure the rejected
mockLoadPersistedTerminals resolution is processed and any fallback creation
(createDefaultTerminal -> mockTerminalSpawn /
mockTerminalStoreState.addTerminal) would have had a chance to run; after
calling rejectProjectALayout(new Error(...)) await the promise microtask flush
(e.g., await act(() => Promise.resolve()) or simply await waitFor(() => {/*
no-op to let promise settle */})) and then replace the final synchronous expects
with waitFor-based assertions like await waitFor(() =>
expect(mockTerminalSpawn).not.toHaveBeenCalled()) and await waitFor(() =>
expect(mockTerminalStoreState.addTerminal).not.toHaveBeenCalled()), referencing
useTerminalRestore, mockLoadPersistedTerminals, createDefaultTerminal,
mockTerminalSpawn, and mockSetTerminalRestoreInProgress so the test reliably
fails if a fallback terminal is spawned.

In `@src/renderer/hooks/use-terminal-restore.ts`:
- Around line 271-293: The workspace tab is activated but
terminalStore.activeTerminalId is never updated, leaving actions targeting the
old terminal; after determining terminalIdToSelect and calling
workspaceStore.setActiveTab (using selectTerminalForProject,
workspaceStore.getActivePaneLeaf, terminalTabId), also retrieve the terminal
store (e.g. useTerminalStore.getState() or terminalStore) and set its active
terminal to terminalIdToSelect (via the setter or by assigning activeTerminalId)
so the terminal store selection is kept in sync with the restored live tab;
perform this update only when terminalIdToSelect and activePane are present and
after the isCancelled() check.

---

Outside diff comments:
In `@src/renderer/hooks/use-terminal-restore.ts`:
- Around line 298-307: The restore flow in useTerminalRestore still awaits
restoreFromLayout(projectIdToRestore, layout) and
createDefaultTerminal(projectIdToRestore) even after the isCancelled() guard, so
those helpers can continue async work and mutate stores for a stale project;
update restoreFromLayout and createDefaultTerminal to check cancellation (use
the same isCancelled or callId guard) before every store mutation and
immediately after any awaited spawn/long async step, returning early if
cancelled to prevent spawning PTYs or re-adding terminals for the wrong project
in useTerminalRestore.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 129f3f20-26bb-46f8-911e-9d995879e77d

📥 Commits

Reviewing files that changed from the base of the PR and between 7784a08 and 48fbcdc.

📒 Files selected for processing (3)
  • src/renderer/hooks/use-editor-persistence.test.ts
  • src/renderer/hooks/use-terminal-restore.test.ts
  • src/renderer/hooks/use-terminal-restore.ts

Sync live terminal selection with workspace activation and guard restore helpers against stale async mutations. Tighten restore regression tests so cancelled errors cannot spawn fallback terminals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gnoviawan gnoviawan added phase-1 Phase 1: Stability & Polish (v0.3.1) v0.3.1 labels Mar 9, 2026
Update editor persistence expectations to match the current project-switch restore sequence and latest-call behavior. Keep the full Vitest suite green after the terminal restore follow-up changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/hooks/use-terminal-restore.ts (1)

665-673: ⚠️ Potential issue | 🟠 Major

Same PTY leak pattern applies here.

The cancellation check at lines 670-673 has the same issue: if cancelled after spawn succeeds, the PTY is orphaned. Apply the same fix pattern as suggested for restoreFromLayout.

🐛 Proposed fix
     const spawnResult = await terminalApi.spawn({
       shell,
       cwd: project?.path
     })

     if (isCancelled()) {
       debugLog('createDefaultTerminal', `CANCELLED [${defaultId}] after spawn`)
+      // Clean up the spawned PTY to prevent resource leak
+      if (spawnResult.success) {
+        terminalApi.kill(spawnResult.data.id).catch((err) => {
+          debugLog('createDefaultTerminal', `Failed to kill orphaned PTY on cancel`, { err })
+        })
+      }
       return
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/use-terminal-restore.ts` around lines 665 - 673, After
spawning the PTY in createDefaultTerminal (spawnResult from terminalApi.spawn)
you must clean up the spawned PTY if isCancelled() returns true to avoid
orphaning the PTY; mirror the fix used in restoreFromLayout by calling the
terminal cleanup API (e.g. terminalApi.kill or the appropriate dispose/close
method) with the identifier returned on spawn (spawnResult.id or
spawnResult.pid) before logging the cancellation
(debugLog('createDefaultTerminal'...)) and returning.
🧹 Nitpick comments (1)
src/renderer/hooks/use-editor-persistence.test.ts (1)

516-540: Third mock is unused — consider removing for clarity.

The test expects exactly 2 calls to mockPersistenceRead (line 571), but three mocks are chained. The third mock (lines 524-540) is never invoked. While this doesn't affect test correctness, removing it would reduce confusion.

♻️ Suggested simplification
     mockPersistenceRead
       .mockImplementationOnce(
         () =>
           new Promise((resolve) => {
             projectARead.resolve = resolve
           })
       )
       .mockResolvedValueOnce({ success: true, data: null })
-      .mockResolvedValueOnce({
-        success: true,
-        data: {
-          openFiles: [],
-          activeFilePath: null,
-          expandedDirs: ['/projects/b'],
-          fileExplorerVisible: true,
-          activeTabId: null,
-          activePaneId: 'pane-b',
-          paneLayout: {
-            type: 'leaf',
-            id: 'pane-b',
-            tabs: [{ type: 'editor', filePath: '/projects/b/src/index.ts' }],
-            activeTabId: 'edit-/projects/b/src/index.ts'
-          }
-        }
-      })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/use-editor-persistence.test.ts` around lines 516 - 540,
The test chains three responses on mockPersistenceRead but only asserts two
calls; remove the unused third stub to avoid confusion by deleting the
mockResolvedValueOnce call that returns the full editor state (the block passed
to mockPersistenceRead.mockResolvedValueOnce with data containing openFiles,
activeFilePath, expandedDirs, paneLayout, etc.), or alternatively update the
test's expectation if that third response should be consumed; locate the stub on
mockPersistenceRead in use-editor-persistence.test.ts and remove the third
mockResolvedValueOnce entry.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/hooks/use-terminal-restore.ts`:
- Around line 493-538: The code can orphan a spawned PTY if cancellation occurs
after terminalApi.spawn returns but before the terminal is tracked; after
calling terminalApi.spawn (and before mutating idMap/newTerminals or
incrementing SPAWN_CALL_COUNT), check isCancelled() and if true, synchronously
terminate/cleanup the spawned PTY using the terminalApi method that closes/kills
a PTY (pass spawnResult.data.id), ensure TERMINALS_PENDING_PTY_ASSIGNMENT
cleanup still runs, and then continue the loop without incrementing
SPAWN_CALL_COUNT or adding the terminal; reference resolveShellToPath,
normalizeShellForStartup, terminalApi.spawn, spawnResult.data.id,
SPAWN_CALL_COUNT, idMap, newTerminals, and TERMINALS_PENDING_PTY_ASSIGNMENT when
making the change.

---

Outside diff comments:
In `@src/renderer/hooks/use-terminal-restore.ts`:
- Around line 665-673: After spawning the PTY in createDefaultTerminal
(spawnResult from terminalApi.spawn) you must clean up the spawned PTY if
isCancelled() returns true to avoid orphaning the PTY; mirror the fix used in
restoreFromLayout by calling the terminal cleanup API (e.g. terminalApi.kill or
the appropriate dispose/close method) with the identifier returned on spawn
(spawnResult.id or spawnResult.pid) before logging the cancellation
(debugLog('createDefaultTerminal'...)) and returning.

---

Nitpick comments:
In `@src/renderer/hooks/use-editor-persistence.test.ts`:
- Around line 516-540: The test chains three responses on mockPersistenceRead
but only asserts two calls; remove the unused third stub to avoid confusion by
deleting the mockResolvedValueOnce call that returns the full editor state (the
block passed to mockPersistenceRead.mockResolvedValueOnce with data containing
openFiles, activeFilePath, expandedDirs, paneLayout, etc.), or alternatively
update the test's expectation if that third response should be consumed; locate
the stub on mockPersistenceRead in use-editor-persistence.test.ts and remove the
third mockResolvedValueOnce entry.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 95d479f9-51c4-4271-80a0-a292ba9257c3

📥 Commits

Reviewing files that changed from the base of the PR and between 48fbcdc and d8b4489.

📒 Files selected for processing (3)
  • src/renderer/hooks/use-editor-persistence.test.ts
  • src/renderer/hooks/use-terminal-restore.test.ts
  • src/renderer/hooks/use-terminal-restore.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/renderer/hooks/use-terminal-restore.test.ts

Kill spawned PTYs when project switches cancel terminal restoration so background processes are not orphaned. Add regression coverage for both layout restore and default terminal creation cancellation paths.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/renderer/hooks/use-terminal-restore.ts (1)

475-478: ⚠️ Potential issue | 🟠 Major

Clean up already-spawned PTYs on the later cancellation exits too.

The post-spawn kill only covers the PTY from the current iteration. If cancellation happens after one terminal has already been pushed into newTerminals and before the loop finishes or before setTerminals, these early returns still orphan the earlier PTYs.

Also applies to: 548-550

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/use-terminal-restore.ts` around lines 475 - 478, In
restoreFromLayout, when isCancelled() returns true you must clean up any PTYs
already spawned in this run instead of returning immediately; track spawned PTY
objects (e.g., collect them in newTerminals or a temporary spawnedPtys array
inside restoreFromLayout) and call their kill/close method before the early
return, and apply the same cleanup at the other cancellation exit (the later
isCancelled() check around the 548-550 area) so previously pushed terminals are
not orphaned; ensure you still log the CANCELLED [restoreId] message after
performing the cleanup.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/hooks/use-terminal-restore.ts`:
- Around line 290-292: The code currently always uses
workspaceStore.getActivePaneLeaf() when calling workspaceStore.setActiveTab(...
terminalTabId(...)), which can set an activeTabId on the wrong pane if the
restored terminal lives elsewhere; update the logic to locate the pane/leaf that
actually contains the restored tab (e.g. search workspaceStore leaves for a leaf
whose tabs include terminalTabId(terminalIdToSelect) or call a helper like
workspaceStore.findLeafByTabId if available), then call
workspaceStore.setActiveTab(foundLeaf.id, terminalTabId(terminalIdToSelect));
fall back to getActivePaneLeaf() only if no containing leaf is found. Ensure
this change is tied to the restore flow that calls ensureTerminalTab and uses
terminalTabId and setActiveTab.
- Around line 529-530: SPAWN_CALL_COUNT is only incremented and never
decremented, causing the 50-spawn guard to permanently block restores; change
the approach so SPAWN_CALL_COUNT represents active/spawned-but-not-yet-closed
PTYs (or is reset per restore session): increment SPAWN_CALL_COUNT where
currently done (the SPAWN_CALL_COUNT++ at lines like the shown 529 and the other
occurrence near 704), and decrement it when the corresponding PTY/terminal is
closed or exits (hook into the PTY dispose/exit handler or terminal close
callback), or alternatively reset SPAWN_CALL_COUNT = 0 at the start of the
restore loop or on project switch; ensure both increment sites and the chosen
decrement/reset point are updated so the guard does not permanently block
restores.
- Line 223: The current setTerminalRestoreInProgress usage creates a race where
a cancelled restore effect can clear another effect's in-progress flag; modify
setTerminalRestoreInProgress to accept an owner/token (e.g., a symbol or unique
id) when setting the flag and require that same owner/token when clearing it so
only the effect that acquired the flag can release it; update all callers
(including where setTerminalRestoreInProgress(projectIdToRestore, true) is
invoked and the finally blocks that write false) to pass a generated owner token
from the effect (e.g., const owner = Symbol() or uuid) and verify ownership
inside setTerminalRestoreInProgress before removing the projectId from the Set.
Ensure the token is returned/kept by the effect so its finally uses the same
token to clear the flag.

---

Duplicate comments:
In `@src/renderer/hooks/use-terminal-restore.ts`:
- Around line 475-478: In restoreFromLayout, when isCancelled() returns true you
must clean up any PTYs already spawned in this run instead of returning
immediately; track spawned PTY objects (e.g., collect them in newTerminals or a
temporary spawnedPtys array inside restoreFromLayout) and call their kill/close
method before the early return, and apply the same cleanup at the other
cancellation exit (the later isCancelled() check around the 548-550 area) so
previously pushed terminals are not orphaned; ensure you still log the CANCELLED
[restoreId] message after performing the cleanup.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: ed588df6-9973-4449-8878-50224af54e2e

📥 Commits

Reviewing files that changed from the base of the PR and between d8b4489 and d977c73.

📒 Files selected for processing (2)
  • src/renderer/hooks/use-terminal-restore.test.ts
  • src/renderer/hooks/use-terminal-restore.ts

Comment on lines +529 to +530
// FIX #6: Increment SPAWN_CALL_COUNT for each successful spawn in the loop
SPAWN_CALL_COUNT++
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't let the spawn guard turn into a session-lifetime fuse.

SPAWN_CALL_COUNT only grows, but the guard blocks restores once it reaches 50. After enough successful restores/default creations in one session, later project switches stop restoring terminals even if every previous PTY was already closed.

Also applies to: 704-704

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/use-terminal-restore.ts` around lines 529 - 530,
SPAWN_CALL_COUNT is only incremented and never decremented, causing the 50-spawn
guard to permanently block restores; change the approach so SPAWN_CALL_COUNT
represents active/spawned-but-not-yet-closed PTYs (or is reset per restore
session): increment SPAWN_CALL_COUNT where currently done (the
SPAWN_CALL_COUNT++ at lines like the shown 529 and the other occurrence near
704), and decrement it when the corresponding PTY/terminal is closed or exits
(hook into the PTY dispose/exit handler or terminal close callback), or
alternatively reset SPAWN_CALL_COUNT = 0 at the start of the restore loop or on
project switch; ensure both increment sites and the chosen decrement/reset point
are updated so the guard does not permanently block restores.

Track restore ownership so stale effects cannot clear active restore state, reuse the correct pane when reselecting live terminals, and clean up spawned PTYs during cancellation while resetting the restore spawn guard per session.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
src/renderer/hooks/use-terminal-restore.ts (1)

65-93: Consider parallelizing PTY cleanup for faster cancellation.

The sequential for...of loop with await kills PTYs one at a time. For faster cleanup during rapid project switching, these kills could be parallelized since they're independent operations.

♻️ Proposed parallel cleanup
 async function cleanupSpawnedPtys(
   terminals: Array<{ ptyId?: string }>,
   restoreId: string,
   phase: string
 ): Promise<void> {
   const ptyIds = Array.from(
     new Set(
       terminals
         .map((terminal) => terminal.ptyId)
         .filter((ptyId): ptyId is string => !!ptyId)
     )
   )

   if (ptyIds.length === 0) {
     return
   }

   debugLog('restoreFromLayout', `CLEANING UP SPAWNED PTYS [${restoreId}]`, {
     phase,
     ptyIds
   })

-  for (const ptyId of ptyIds) {
-    const killResult = await terminalApi.kill(ptyId)
-    if (!killResult.success) {
-      console.error('Failed to kill cancelled restore PTY:', killResult.error)
-    }
-  }
+  await Promise.all(
+    ptyIds.map(async (ptyId) => {
+      const killResult = await terminalApi.kill(ptyId)
+      if (!killResult.success) {
+        console.error('Failed to kill cancelled restore PTY:', killResult.error)
+      }
+    })
+  )
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/use-terminal-restore.ts` around lines 65 - 93, The
cleanupSpawnedPtys function currently kills PTYs sequentially which is slow;
change it to run terminalApi.kill calls in parallel by mapping ptyIds to kill
promises and awaiting Promise.all (or Promise.allSettled) so all kills execute
concurrently, then handle/log failures per pty (using killResult.success or
rejected promise reason) to preserve error reporting; reference the function
cleanupSpawnedPtys and the terminalApi.kill calls when making this change.
src/renderer/hooks/useTerminalAutoSave.test.ts (1)

40-51: Add state cleanup to prevent test pollution.

The setTerminalRestoreInProgress tests mutate module-level state (terminalRestoreProjectsInProgress map) but don't clean up after themselves. If future tests in this file depend on isTerminalRestoreInProgress() returning false, they could fail intermittently depending on test execution order.

♻️ Proposed fix to add cleanup
 describe('setTerminalRestoreInProgress', () => {
+  afterEach(() => {
+    // Clean up any lingering restore state
+    setTerminalRestoreInProgress('proj-1', false, 'owner-a')
+    setTerminalRestoreInProgress('proj-1', false, 'owner-b')
+  })
+
   it('only clears restore state for the matching owner', () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/useTerminalAutoSave.test.ts` around lines 40 - 51, The
test mutates module-level terminalRestoreProjectsInProgress via
setTerminalRestoreInProgress and doesn't reset it; add cleanup to avoid
pollution by clearing/resetting state after each test (or at the end of this
`describe`): call the appropriate reset/clear sequence so
isTerminalRestoreInProgress() returns false for subsequent tests (e.g., clear
terminalRestoreProjectsInProgress or call setTerminalRestoreInProgress for
affected owners to false) and ensure this runs in an afterEach hook adjacent to
the describe containing setTerminalRestoreInProgress and
isTerminalRestoreInProgress.
src/renderer/hooks/useTerminalAutoSave.ts (1)

20-22: Consider adding debug logging for unexpected falsy parameters.

The silent early return when projectId or ownerId is falsy could mask programming errors. Adding a debug log would help diagnose issues during development without affecting production.

♻️ Proposed enhancement
 export function setTerminalRestoreInProgress(
   projectId: string,
   isRestoring: boolean,
   ownerId: string
 ): void {
   if (!projectId || !ownerId) {
+    if (import.meta.env.DEV) {
+      console.warn('setTerminalRestoreInProgress called with falsy params:', { projectId, ownerId })
+    }
     return
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/hooks/useTerminalAutoSave.ts` around lines 20 - 22, In
useTerminalAutoSave (the useTerminalAutoSave hook) the early return when
projectId or ownerId is falsy is silent; update the guard to log a debug message
(e.g., using the project's logger or console.debug) that includes which
parameter(s) are missing and their values so developers can diagnose unexpected
falsy inputs; ensure the log is at a debug/verbose level and keep the existing
early return behavior unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/renderer/hooks/use-terminal-restore.ts`:
- Around line 65-93: The cleanupSpawnedPtys function currently kills PTYs
sequentially which is slow; change it to run terminalApi.kill calls in parallel
by mapping ptyIds to kill promises and awaiting Promise.all (or
Promise.allSettled) so all kills execute concurrently, then handle/log failures
per pty (using killResult.success or rejected promise reason) to preserve error
reporting; reference the function cleanupSpawnedPtys and the terminalApi.kill
calls when making this change.

In `@src/renderer/hooks/useTerminalAutoSave.test.ts`:
- Around line 40-51: The test mutates module-level
terminalRestoreProjectsInProgress via setTerminalRestoreInProgress and doesn't
reset it; add cleanup to avoid pollution by clearing/resetting state after each
test (or at the end of this `describe`): call the appropriate reset/clear
sequence so isTerminalRestoreInProgress() returns false for subsequent tests
(e.g., clear terminalRestoreProjectsInProgress or call
setTerminalRestoreInProgress for affected owners to false) and ensure this runs
in an afterEach hook adjacent to the describe containing
setTerminalRestoreInProgress and isTerminalRestoreInProgress.

In `@src/renderer/hooks/useTerminalAutoSave.ts`:
- Around line 20-22: In useTerminalAutoSave (the useTerminalAutoSave hook) the
early return when projectId or ownerId is falsy is silent; update the guard to
log a debug message (e.g., using the project's logger or console.debug) that
includes which parameter(s) are missing and their values so developers can
diagnose unexpected falsy inputs; ensure the log is at a debug/verbose level and
keep the existing early return behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 50d291b0-742d-41b3-b812-b89269b79ef1

📥 Commits

Reviewing files that changed from the base of the PR and between d977c73 and 0a81f9c.

📒 Files selected for processing (4)
  • src/renderer/hooks/use-terminal-restore.test.ts
  • src/renderer/hooks/use-terminal-restore.ts
  • src/renderer/hooks/useTerminalAutoSave.test.ts
  • src/renderer/hooks/useTerminalAutoSave.ts

@gnoviawan gnoviawan merged commit 01eaba1 into dev Mar 9, 2026
10 checks passed
@coderabbitai coderabbitai bot mentioned this pull request Mar 10, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

phase-1 Phase 1: Stability & Polish (v0.3.1) v0.3.1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant