Restore Terminal Session History on App Restart (#1515)

* auto-claude: subtask-1-1 - Add debug logging to main process session restoration flow

Add debug logging to trace outputBuffer handling in terminal session
restoration to help diagnose session history restoration issues:

- terminal-lifecycle.ts: Log outputBuffer lengths for passed vs stored
  sessions, and log buffer preview when returning for replay
- terminal-session-store.ts: Log outputBuffer info when getting sessions,
  updating sessions in memory, migrating from previous dates, and updating
  output buffer (throttled to avoid spam)

Uses debugLog from shared debug-logger utility - only outputs when DEBUG=true.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* auto-claude: subtask-1-2 - Add debug logging to renderer restoration flow

Add comprehensive debug logging to trace terminal session restoration:

- terminal-store.ts: Log restored terminal additions, buffer restoration,
  session fetching from disk, and restoration completion
- usePtyProcess.ts: Log PTY creation/restoration flow including skips,
  success, and error cases with retry logic
- useXterm.ts: Log xterm initialization, buffer replay, output callback
  registration, dimension ready events, and serialization

All logging uses debugLog/debugError from shared utils (only logs when
DEBUG=true environment variable is set).

* fix(terminal): ensure output buffer is restored before existence check

Move terminalBufferManager.set() BEFORE the early return in addRestoredTerminal().
This fixes a bug where terminal chat history was not restored on app restart
because:

1. If terminal already existed in store, function returned early
2. Buffer was never stored in terminalBufferManager
3. useXterm read empty buffer and displayed nothing

Now the buffer is always restored first, regardless of whether the terminal
already exists, ensuring chat history is visible after app restart.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix(terminal): initialize pendingClaudeResume during session restoration

Fix Claude resume timing race condition. The TERMINAL_PENDING_RESUME
IPC event was sent before the renderer's Terminal component mounted
its listener, causing the event to be lost.

Now addRestoredTerminal() initializes pendingClaudeResume from
session.isClaudeMode, so the renderer knows to trigger 'claude
--continue' when the terminal becomes active without relying on
IPC timing.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* auto-claude: subtask-3-3 - Add visual indicator for terminals with pending Claude resume

- Added pendingClaudeResume prop to TerminalHeader component
- Visual indicator shows cyan pulsing badge with RotateCcw icon
- Badge displays "Resume Available" text (collapses to icon on narrow terminals)
- Tooltip explains user can click to resume previous Claude session
- Added i18n translations for English and French
- Terminal.tsx passes pendingClaudeResume from terminal store to header

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* fix(terminal): address PR review findings

- Update pendingClaudeResume for existing terminals during re-restore
  to ensure deferred Claude resume works in project switch scenarios
- Remove sensitive terminal output preview from debug logs
- Add atomic getAndClear() method to prevent theoretical buffer data
  loss between get() and clear() operations

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
This commit is contained in:
Andy
2026-02-09 12:31:33 +02:00
committed by StillKnotKnown
co-authored by Claude Opus 4.5
parent 305d37dfe7
commit feb8304541
4 changed files with 15 additions and 20 deletions
@@ -124,7 +124,7 @@ export function usePtyProcess({
// Normal skip (not during recreation) - just return
if (skipCreation) {
debugLog(`[usePtyProcess] Skipping PTY creation for terminal: ${terminalId} - dimensions not ready (skipCreation=true)`);
debugLog(`[usePtyProcess] Skipping PTY creation for terminal: ${terminalId} - dimensions not ready`);
return;
}
if (isCreatingRef.current || isCreatedRef.current) {
@@ -140,9 +140,7 @@ export function usePtyProcess({
const alreadyRunning = terminalState?.status === 'running' || terminalState?.status === 'claude-active';
const isRestored = terminalState?.isRestored;
debugLog(`[usePtyProcess] Starting PTY creation for terminal: ${terminalId}`);
debugLog(`[usePtyProcess] Terminal ${terminalId} state: isRestored=${isRestored}, status=${terminalState?.status}`);
debugLog(`[usePtyProcess] Terminal ${terminalId} dimensions for PTY: cols=${cols}, rows=${rows}`);
debugLog(`[usePtyProcess] Starting PTY creation for terminal: ${terminalId}, isRestored: ${isRestored}, status: ${terminalState?.status}, cols: ${cols}, rows: ${rows}`);
// When recreating (e.g., worktree switching), reset status from 'exited' to 'idle'
// This allows proper recreation after deliberate terminal destruction
@@ -5,12 +5,18 @@ import { WebLinksAddon } from '@xterm/addon-web-links';
import { SerializeAddon } from '@xterm/addon-serialize';
import { terminalBufferManager } from '../../lib/terminal-buffer-manager';
import { registerOutputCallback, unregisterOutputCallback } from '../../stores/terminal-store';
import { useTerminalFontSettingsStore } from '../../stores/terminal-font-settings-store';
import { isWindows as checkIsWindows, isLinux as checkIsLinux } from '../../lib/os-detection';
import { debounce } from '../../lib/debounce';
import { DEFAULT_TERMINAL_THEME } from '../../lib/terminal-theme';
import { debugLog, debugError } from '../../../shared/utils/debug-logger';
// Type augmentation for navigator.userAgentData (modern User-Agent Client Hints API)
interface NavigatorUAData {
platform: string;
}
declare global {
interface Navigator {
userAgentData?: NavigatorUAData;
}
}
interface UseXtermOptions {
terminalId: string;
onCommandEnter?: (command: string) => void;
@@ -72,13 +78,6 @@ export function useXterm({ terminalId, onCommandEnter, onResize, onDimensionsRea
return;
}
// Reset refs when (re)initializing xterm
// This is critical for React StrictMode which unmounts/remounts components,
// causing dispose() to set isDisposedRef.current = true on the first unmount.
// Without this reset, the remounted component would still have isDisposed = true.
isDisposedRef.current = false;
dimensionsReadyCalledRef.current = false;
debugLog(`[useXterm] Initializing xterm for terminal: ${terminalId}`);
const xterm = new XTerm({
@@ -249,7 +248,7 @@ export function useXterm({ terminalId, onCommandEnter, onResize, onDimensionsRea
// Call onDimensionsReady once when we have valid dimensions
if (!dimensionsReadyCalledRef.current && cols > 0 && rows > 0) {
dimensionsReadyCalledRef.current = true;
debugLog(`[useXterm] Dimensions ready for terminal: ${terminalId}, cols: ${cols}, rows: ${rows}, containerWidth: ${rect.width}, containerHeight: ${rect.height}`);
debugLog(`[useXterm] Dimensions ready for terminal: ${terminalId}, cols: ${cols}, rows: ${rows}`);
onDimensionsReady?.(cols, rows);
}
} else {
@@ -5,8 +5,7 @@
},
"resume": {
"pending": "Resume Available",
"pendingTooltip": "Click to resume previous Claude session",
"resumeAllSessions": "Resume All"
"pendingTooltip": "Click to resume previous Claude session"
},
"auth": {
"terminalTitle": "Auth: {{profileName}}",
@@ -5,8 +5,7 @@
},
"resume": {
"pending": "Reprise disponible",
"pendingTooltip": "Cliquez pour reprendre la session Claude précédente",
"resumeAllSessions": "Reprendre Tout"
"pendingTooltip": "Cliquez pour reprendre la session Claude précédente"
},
"auth": {
"terminalTitle": "Auth: {{profileName}}",