Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c2adf89
auto-claude: subtask-1-1 - Create terminal-machine.ts XState v5 state…
AndyMik90 Feb 13, 2026
d78fac5
auto-claude: subtask-1-2 - Add TerminalSwapState interface and swapSt…
AndyMik90 Feb 13, 2026
2c63663
auto-claude: subtask-1-3 - Update shared/state-machines/index.ts barr…
AndyMik90 Feb 13, 2026
839893c
auto-claude: subtask-2-1 - Add migratedSession option to resumeClaude…
AndyMik90 Feb 13, 2026
4d74fd1
auto-claude: subtask-2-2 - Update terminal-manager with swap orchestr…
AndyMik90 Feb 13, 2026
96a2648
auto-claude: subtask-2-3 - Add isClaudeMode to TERMINAL_PROFILE_CHANG…
AndyMik90 Feb 13, 2026
531f7f8
auto-claude: subtask-3-1 - Auto-resume Claude session after terminal …
AndyMik90 Feb 13, 2026
aef8a68
auto-claude: subtask-3-2 - Integrate XState terminal machine into ter…
AndyMik90 Feb 13, 2026
1d06ad1
auto-claude: subtask-4-1 - Add i18n translation keys for swap/resume …
AndyMik90 Feb 13, 2026
2062c4f
auto-claude: subtask-4-2 - Write comprehensive unit tests for termina…
AndyMik90 Feb 13, 2026
ad2a319
Fix PR review findings: XState machine wiring, dead code, YOLO mode p…
AndyMik90 Feb 14, 2026
2874cd0
Merge branch 'develop' into auto-claude/229-implement-account-aware-t…
AndyMik90 Feb 14, 2026
dea5aeb
Fix follow-up review: security, dead code, XState wiring completeness
AndyMik90 Feb 14, 2026
962cfd8
Merge branch 'develop' into auto-claude/229-implement-account-aware-t…
AndyMik90 Feb 14, 2026
a4047ed
Fix follow-up review: security, dead code, XState wiring completeness
AndyMik90 Feb 14, 2026
5f66bde
fix: address review findings for PR #1819
AndyMik90 Feb 15, 2026
e7f9908
Merge remote-tracking branch 'origin/develop' into auto-claude/229-im…
AndyMik90 Feb 15, 2026
93d3389
fix: resolve PR #1819 review findings - session ID preservation and I…
AndyMik90 Feb 15, 2026
70af403
fix: address remaining PR #1819 code quality findings
AndyMik90 Feb 15, 2026
0d613f4
fix: address all PR #1819 review findings (round 7)
AndyMik90 Feb 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions apps/frontend/src/main/claude-profile/session-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
* and can be copied between profiles to enable session continuity after profile switches.
*/

import { existsSync, mkdirSync, copyFileSync, cpSync, unlinkSync } from 'fs';
import { existsSync } from 'fs';
import { mkdir, copyFile, cp, unlink } from 'fs/promises';
import { join, dirname } from 'path';
import { homedir } from 'os';
import { isNodeError } from '../utils/type-guards';
Expand Down Expand Up @@ -95,12 +96,12 @@ export interface SessionMigrationResult {
* @param sessionId - The session UUID to migrate
* @returns Migration result with success status and details
*/
export function migrateSession(
export async function migrateSession(
sourceConfigDir: string,
targetConfigDir: string,
cwd: string,
sessionId: string
): SessionMigrationResult {
): Promise<SessionMigrationResult> {
const result: SessionMigrationResult = {
success: false,
sessionId,
Expand All @@ -118,13 +119,13 @@ export function migrateSession(
try {
// Ensure target directory exists (do this first, before any file operations)
const targetParentDir = dirname(targetFile);
mkdirSync(targetParentDir, { recursive: true });
await mkdir(targetParentDir, { recursive: true });
console.warn('[SessionUtils] Ensured target directory exists:', targetParentDir);

// Attempt to copy the session .jsonl file
// This will throw if source doesn't exist or target cannot be written
try {
copyFileSync(sourceFile, targetFile);
await copyFile(sourceFile, targetFile);
result.filesCopied++;
console.warn('[SessionUtils] Copied session file:', sourceFile, '->', targetFile);
} catch (copyError) {
Expand Down Expand Up @@ -153,7 +154,7 @@ export function migrateSession(
// Attempt to copy the session directory (tool-results) if it exists
// Use try-catch instead of existsSync to avoid TOCTOU race
try {
cpSync(sourceDir, targetDir, { recursive: true });
await cp(sourceDir, targetDir, { recursive: true });
result.filesCopied++;
console.warn('[SessionUtils] Copied session directory:', sourceDir, '->', targetDir);
} catch (dirCopyError) {
Expand Down Expand Up @@ -182,7 +183,7 @@ export function migrateSession(
// Clean up partially migrated session file to enable retry
// Use try-catch instead of existsSync to avoid TOCTOU race
try {
unlinkSync(targetFile);
await unlink(targetFile);
console.warn('[SessionUtils] Cleaned up partial migration file:', targetFile);
} catch (cleanupError) {
// If file doesn't exist during cleanup, that's fine
Expand Down
18 changes: 14 additions & 4 deletions apps/frontend/src/main/ipc-handlers/terminal-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,8 @@ export function registerTerminalHandlers(
id: string;
sessionId?: string;
sessionMigrated?: boolean;
isClaudeMode?: boolean;
dangerouslySkipPermissions?: boolean;
}> = [];

// Process each terminal
Expand All @@ -273,7 +275,7 @@ export function registerTerminalHandlers(
to: targetConfigDir
});

const migrationResult = migrateSession(
const migrationResult = await migrateSession(
sourceConfigDir,
targetConfigDir,
terminal.cwd,
Expand All @@ -284,11 +286,19 @@ export function registerTerminalHandlers(
debugLog('[terminal-handlers:CLAUDE_PROFILE_SET_ACTIVE] Session migration result:', migrationResult);
}

// Store YOLO mode flag server-side for migrated sessions
// (consumed by resumeClaudeAsync when the new terminal resumes)
if (sessionMigrated && terminal.claudeSessionId && terminal.dangerouslySkipPermissions) {
terminalManager.storeMigratedSessionFlag(terminal.claudeSessionId, terminal.dangerouslySkipPermissions);
}

// All terminals need refresh (PTY env vars can't be updated)
terminalsNeedingRefresh.push({
id: terminal.id,
sessionId: terminal.claudeSessionId,
sessionMigrated
sessionMigrated,
isClaudeMode: terminal.isClaudeMode,
dangerouslySkipPermissions: terminal.dangerouslySkipPermissions
});
}

Expand Down Expand Up @@ -613,9 +623,9 @@ export function registerTerminalHandlers(

ipcMain.on(
IPC_CHANNELS.TERMINAL_RESUME_CLAUDE,
(_, id: string, sessionId?: string) => {
(_, id: string, sessionId?: string, options?: { migratedSession?: boolean }) => {
// Use async version to avoid blocking main process during CLI detection
terminalManager.resumeClaudeAsync(id, sessionId).catch((error) => {
terminalManager.resumeClaudeAsync(id, sessionId, options).catch((error) => {
console.warn('[terminal-handlers] Failed to resume Claude:', error);
});
}
Expand Down
16 changes: 12 additions & 4 deletions apps/frontend/src/main/terminal/claude-integration-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1386,7 +1386,8 @@ export async function invokeClaudeAsync(
export async function resumeClaudeAsync(
terminal: TerminalProcess,
sessionId: string | undefined,
getWindow: WindowGetter
getWindow: WindowGetter,
options?: { migratedSession?: boolean }
): Promise<void> {
// Track terminal state for cleanup on error
const wasClaudeMode = terminal.isClaudeMode;
Expand Down Expand Up @@ -1419,12 +1420,19 @@ export async function resumeClaudeAsync(
// and we don't want stale IDs persisting through SessionHandler.persistSessionAsync().
terminal.claudeSessionId = undefined;

// Deprecation warning for callers still passing sessionId
if (sessionId) {
// Deprecation warning for callers still passing sessionId (skip for migrated sessions)
if (sessionId && !options?.migratedSession) {
console.warn('[ClaudeIntegration:resumeClaudeAsync] sessionId parameter is deprecated and ignored; using claude --continue instead');
}

const command = `${pathPrefix}${escapedClaudeCmd} --continue`;
if (options?.migratedSession) {
debugLog('[ClaudeIntegration:resumeClaudeAsync] Post-swap resume for terminal:', terminal.id);
}

// Preserve YOLO mode flag from terminal's stored state
const extraFlags = terminal.dangerouslySkipPermissions ? YOLO_MODE_FLAG : '';

const command = `${pathPrefix}${escapedClaudeCmd} --continue${extraFlags}`;

// Use PtyManager.writeToPty for safer write with error handling
PtyManager.writeToPty(terminal, `${command}\r`);
Expand Down
37 changes: 33 additions & 4 deletions apps/frontend/src/main/terminal/terminal-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
TerminalProcess,
WindowGetter,
TerminalOperationResult,
TerminalProfileChangeInfo
TerminalProfileChangeInfo,
} from './types';
import * as PtyManager from './pty-manager';
import * as SessionHandler from './session-handler';
Expand All @@ -26,6 +26,8 @@ export class TerminalManager {
private saveTimer: NodeJS.Timeout | null = null;
private lastNotifiedRateLimitReset: Map<string, string> = new Map();
private eventCallbacks: TerminalEventHandler.EventHandlerCallbacks;
/** Server-side storage for YOLO mode flags during profile migration (sessionId → flag) */
private migratedSessionFlags: Map<string, boolean> = new Map();
Comment on lines +29 to +30
Copy link
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Minor: migratedSessionFlags entries may linger if a migrated session is never resumed.

Entries are consumed in resumeClaudeAsync (line 241) and bulk-cleared in killAll (line 119), but if a terminal is destroyed individually before resuming (e.g., user closes it), the stale entry persists until the next killAll. Practically this is a handful of Map entries at most, so low impact, but consider also clearing entries in destroy() if the terminal had a pending flag.

🤖 Prompt for AI Agents
In `@apps/frontend/src/main/terminal/terminal-manager.ts` around lines 29 - 30,
migratedSessionFlags can retain entries if a terminal with a pending migration
flag is destroyed without resuming; update the Terminal.destroy (or equivalent
terminal cleanup) to check for and remove the session's key from
migratedSessionFlags (same Map referenced by resumeClaudeAsync and killAll) when
a terminal is torn down, ensuring any sessionId associated with this terminal is
cleared to avoid stale entries.


constructor(getWindow: WindowGetter) {
this.getWindow = getWindow;
Expand Down Expand Up @@ -101,6 +103,12 @@ export class TerminalManager {
* Destroy a terminal process
*/
async destroy(id: string): Promise<TerminalOperationResult> {
// Clean up migrated session flags if this terminal has a pending migrated session
const terminal = this.terminals.get(id);
if (terminal?.claudeSessionId) {
this.migratedSessionFlags.delete(terminal.claudeSessionId);
}
Comment on lines +106 to +110
Copy link

Choose a reason for hiding this comment

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

Bug: During a profile swap, destroyTerminal() prematurely deletes the dangerouslySkipPermissions flag from migratedSessionFlags before the new terminal can use it, causing the flag to be lost.
Severity: HIGH

Suggested Fix

The flag should not be deleted from migratedSessionFlags within the terminalManager.destroy method. The cleanup of this flag should be deferred until after the new terminal has successfully resumed the session and consumed the flag, or it should be removed from the destroy logic altogether if another mechanism handles its lifecycle.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: apps/frontend/src/main/terminal/terminal-manager.ts#L106-L110

Potential issue: During a profile swap, a race condition occurs where the
`dangerouslySkipPermissions` flag (YOLO mode) is prematurely deleted. The
`destroyTerminal` function for the old terminal is called and awaited, which cleans up
the flag from the `migratedSessionFlags` map. Immediately after, a new terminal is
created and attempts to resume the session by looking for the same flag in the map.
Because the flag was already deleted, the new terminal fails to restore the YOLO mode,
silently reverting to standard permission behavior and potentially causing unexpected
permission prompts for the user.


return TerminalLifecycle.destroyTerminal(
id,
this.terminals,
Expand All @@ -114,6 +122,7 @@ export class TerminalManager {
* Kill all terminal processes
*/
async killAll(): Promise<void> {
this.migratedSessionFlags.clear();
this.saveTimer = await TerminalLifecycle.destroyAllTerminals(
this.terminals,
this.saveTimer
Expand Down Expand Up @@ -223,13 +232,32 @@ export class TerminalManager {
/**
* Resume Claude in a terminal asynchronously (non-blocking)
*/
async resumeClaudeAsync(id: string, sessionId?: string): Promise<void> {
async resumeClaudeAsync(id: string, sessionId?: string, options?: { migratedSession?: boolean }): Promise<void> {
const terminal = this.terminals.get(id);
if (!terminal) {
return;
}

await ClaudeIntegration.resumeClaudeAsync(terminal, sessionId, this.getWindow);
// For migrated sessions, restore YOLO mode from server-side storage
// (set during profile change in storeMigratedSessionFlag)
if (options?.migratedSession && sessionId) {
const storedFlag = this.migratedSessionFlags.get(sessionId);
if (storedFlag !== undefined) {
terminal.dangerouslySkipPermissions = storedFlag;
this.migratedSessionFlags.delete(sessionId);
}
}

await ClaudeIntegration.resumeClaudeAsync(terminal, sessionId, this.getWindow, options);
}

/**
* Store YOLO mode flag for a session being migrated during profile swap.
* Called from the profile change handler before the renderer recreates terminals.
* The flag is consumed by resumeClaudeAsync when the new terminal resumes.
*/
storeMigratedSessionFlag(sessionId: string, dangerouslySkipPermissions: boolean): void {
this.migratedSessionFlags.set(sessionId, dangerouslySkipPermissions);
}

/**
Expand Down Expand Up @@ -387,7 +415,8 @@ export class TerminalManager {
projectPath: terminal.projectPath,
claudeSessionId: terminal.claudeSessionId,
claudeProfileId: terminal.claudeProfileId,
isClaudeMode: terminal.isClaudeMode
isClaudeMode: terminal.isClaudeMode,
dangerouslySkipPermissions: terminal.dangerouslySkipPermissions
});
}

Expand Down
1 change: 1 addition & 0 deletions apps/frontend/src/main/terminal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,5 @@ export interface TerminalProfileChangeInfo {
claudeSessionId?: string;
claudeProfileId?: string;
isClaudeMode: boolean;
dangerouslySkipPermissions?: boolean;
}
6 changes: 3 additions & 3 deletions apps/frontend/src/preload/api/terminal-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export interface TerminalAPI {
rows?: number
) => Promise<IPCResult<import('../../shared/types').TerminalRestoreResult>>;
clearTerminalSessions: (projectPath: string) => Promise<IPCResult>;
resumeClaudeInTerminal: (id: string, sessionId?: string) => void;
resumeClaudeInTerminal: (id: string, sessionId?: string, options?: { migratedSession?: boolean }) => void;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

TerminalAPI interface is missing dangerouslySkipPermissions in the options type.

The interface declares options?: { migratedSession?: boolean }, but the implementation at line 169 and the ElectronAPI in ipc.ts (line 255) both include dangerouslySkipPermissions?: boolean. Callers using the TerminalAPI type won't see the flag in autocomplete/type hints.

🐛 Proposed fix
-  resumeClaudeInTerminal: (id: string, sessionId?: string, options?: { migratedSession?: boolean }) => void;
+  resumeClaudeInTerminal: (id: string, sessionId?: string, options?: { migratedSession?: boolean; dangerouslySkipPermissions?: boolean }) => void;
🤖 Prompt for AI Agents
In `@apps/frontend/src/preload/api/terminal-api.ts` at line 50, The TerminalAPI
interface's resumeClaudeInTerminal signature omits the
dangerouslySkipPermissions flag in its options type; update the options type for
resumeClaudeInTerminal in TerminalAPI to include dangerouslySkipPermissions?:
boolean (matching the implementation used by resumeClaudeInTerminal and the
ElectronAPI definition) so callers get correct autocomplete and type-checking
for that flag.

activateDeferredClaudeResume: (id: string) => void;
getTerminalSessionDates: (projectPath?: string) => Promise<IPCResult<import('../../shared/types').SessionDateInfo[]>>;
getTerminalSessionsForDate: (
Expand Down Expand Up @@ -166,8 +166,8 @@ export const createTerminalAPI = (): TerminalAPI => ({
clearTerminalSessions: (projectPath: string): Promise<IPCResult> =>
ipcRenderer.invoke(IPC_CHANNELS.TERMINAL_CLEAR_SESSIONS, projectPath),

resumeClaudeInTerminal: (id: string, sessionId?: string): void =>
ipcRenderer.send(IPC_CHANNELS.TERMINAL_RESUME_CLAUDE, id, sessionId),
resumeClaudeInTerminal: (id: string, sessionId?: string, options?: { migratedSession?: boolean }): void =>
ipcRenderer.send(IPC_CHANNELS.TERMINAL_RESUME_CLAUDE, id, sessionId, options),

activateDeferredClaudeResume: (id: string): void =>
ipcRenderer.send(IPC_CHANNELS.TERMINAL_ACTIVATE_DEFERRED_RESUME, id),
Expand Down
26 changes: 16 additions & 10 deletions apps/frontend/src/renderer/hooks/useTerminalProfileChange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { debugLog, debugError } from '../../shared/utils/debug-logger';
* Hook to handle terminal profile change events.
* When a Claude profile switches, all terminals need to be recreated with the new profile's
* environment variables. Terminals with active Claude sessions will have their sessions
* migrated and can be resumed with --resume {sessionId}.
* migrated and automatically resumed with --continue.
*/
export function useTerminalProfileChange(): void {
// Track terminals being recreated to prevent duplicate processing
Expand Down Expand Up @@ -101,19 +101,25 @@ export function useTerminalProfileChange(): void {
newId: newTerminal.id
});

// If there was an active Claude session that was migrated, show a message
// and set up for potential resume
// If there was an active Claude session that was migrated, auto-resume it
if (sessionId && sessionMigrated) {
debugLog('[useTerminalProfileChange] Session migrated, ready for resume:', sessionId);
// Store the session ID so the user can resume if desired
debugLog('[useTerminalProfileChange] Session migrated, auto-resuming:', sessionId);
// Store the session ID for tracking
store.setClaudeSessionId(newTerminal.id, sessionId);
// Set pending resume flag - user can trigger resume from terminal tab
store.setPendingClaudeResume(newTerminal.id, true);
// Send a message to the terminal about the session
window.electronAPI.sendTerminalInput(

// Auto-resume the Claude session with --continue
// YOLO mode (dangerouslySkipPermissions) is preserved server-side by the
// main process during migration (storeMigratedSessionFlag), so resumeClaudeAsync
// will restore it automatically when migratedSession is true
// Note: resumeClaudeInTerminal uses fire-and-forget IPC (ipcRenderer.send),
// so errors in the main process cannot be caught here. The main process will
// emit onTerminalPendingResume if the resume fails, triggering deferred resume.
window.electronAPI.resumeClaudeInTerminal(
Comment on lines 112 to 121

This comment was marked as outdated.

newTerminal.id,
`# Profile switched. Previous Claude session available.\n# Run: claude --resume ${sessionId}\n`
sessionId,
{ migratedSession: true }
);
Comment on lines 110 to 125

This comment was marked as outdated.

debugLog('[useTerminalProfileChange] Resume initiated for terminal:', newTerminal.id);
}

} finally {
Expand Down
Loading
Loading