From 1302c61b71c5b5f17cbc4792d2bf5455543825c5 Mon Sep 17 00:00:00 2001 From: AndyMik90 Date: Sat, 14 Feb 2026 00:07:18 +0100 Subject: [PATCH] auto-claude: subtask-3-2 - Integrate XState terminal machine into terminal-store Add module-level terminalActors Map and helper functions (getOrCreateTerminalActor, sendTerminalMachineEvent, deriveTerminalStateFromMachine) for XState actor management. Store actions (setClaudeMode, setClaudeSessionId, setClaudeBusy, setPendingClaudeResume) now forward corresponding events to the XState machine. Actors are cleaned up on terminal removal and clearAllTerminals. Co-Authored-By: Claude Opus 4.6 --- .../src/renderer/stores/terminal-store.ts | 96 ++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/apps/frontend/src/renderer/stores/terminal-store.ts b/apps/frontend/src/renderer/stores/terminal-store.ts index ffdc2469..1fca6890 100644 --- a/apps/frontend/src/renderer/stores/terminal-store.ts +++ b/apps/frontend/src/renderer/stores/terminal-store.ts @@ -1,10 +1,66 @@ import { create } from 'zustand'; +import { createActor } from 'xstate'; +import type { ActorRefFrom, SnapshotFrom } from 'xstate'; import { v4 as uuid } from 'uuid'; import { arrayMove } from '@dnd-kit/sortable'; import type { TerminalSession, TerminalWorktreeConfig } from '../../shared/types'; +import { terminalMachine, type TerminalEvent } from '../../shared/state-machines'; import { terminalBufferManager } from '../lib/terminal-buffer-manager'; import { debugLog, debugError } from '../../shared/utils/debug-logger'; +type TerminalActor = ActorRefFrom; + +/** + * Module-level Map to store terminal ID -> XState actor mappings. + * + * DESIGN NOTE: Stored outside Zustand because actors are mutable references + * that shouldn't be serialized in state. Similar pattern to xtermCallbacks. + */ +const terminalActors = new Map(); + +/** + * Get or create an XState terminal actor for a given terminal ID. + * Actors are lazily created on first access and cached for the terminal's lifetime. + */ +export function getOrCreateTerminalActor(terminalId: string): TerminalActor { + let actor = terminalActors.get(terminalId); + if (!actor) { + actor = createActor(terminalMachine); + actor.start(); + terminalActors.set(terminalId, actor); + debugLog(`[TerminalStore] Created XState actor for terminal: ${terminalId}`); + } + return actor; +} + +/** + * Send an event to a terminal's XState machine. + * Creates the actor if it doesn't exist yet. + */ +export function sendTerminalMachineEvent(terminalId: string, event: TerminalEvent): void { + const actor = getOrCreateTerminalActor(terminalId); + const stateBefore = String(actor.getSnapshot().value); + actor.send(event); + const stateAfter = String(actor.getSnapshot().value); + debugLog(`[TerminalStore] Machine ${terminalId}: ${event.type} (${stateBefore} -> ${stateAfter})`); +} + +/** + * Derive terminal boolean fields from an XState machine snapshot. + * Used to keep legacy boolean fields in sync with the machine state. + */ +export function deriveTerminalStateFromMachine(snapshot: SnapshotFrom): { + isClaudeMode: boolean; + isClaudeBusy: boolean; + pendingClaudeResume: boolean; +} { + const state = String(snapshot.value); + const isClaudeMode = state === 'claude_starting' || state === 'claude_active' || state === 'swapping'; + const isClaudeBusy = snapshot.context.isBusy; + const pendingClaudeResume = state === 'pending_resume'; + return { isClaudeMode, isClaudeBusy, pendingClaudeResume }; +} + /** * Module-level Map to store terminal ID -> xterm write callback mappings. * @@ -301,9 +357,15 @@ export const useTerminalStore = create((set, get) => ({ }, removeTerminal: (id: string) => { - // Clean up buffer manager and output callback + // Clean up buffer manager, output callback, and XState actor terminalBufferManager.dispose(id); xtermCallbacks.delete(id); + const actor = terminalActors.get(id); + if (actor) { + actor.stop(); + terminalActors.delete(id); + debugLog(`[TerminalStore] Cleaned up XState actor for terminal: ${id}`); + } set((state) => { const newTerminals = state.terminals.filter((t) => t.id !== id); @@ -339,6 +401,13 @@ export const useTerminalStore = create((set, get) => ({ }, setClaudeMode: (id: string, isClaudeMode: boolean) => { + // Send corresponding event to XState machine + if (isClaudeMode) { + sendTerminalMachineEvent(id, { type: 'CLAUDE_ACTIVE' }); + } else { + sendTerminalMachineEvent(id, { type: 'CLAUDE_EXITED' }); + } + set((state) => ({ terminals: state.terminals.map((t) => t.id === id @@ -356,6 +425,9 @@ export const useTerminalStore = create((set, get) => ({ }, setClaudeSessionId: (id: string, sessionId: string) => { + // Send CLAUDE_ACTIVE with session ID to XState machine + sendTerminalMachineEvent(id, { type: 'CLAUDE_ACTIVE', claudeSessionId: sessionId }); + set((state) => ({ terminals: state.terminals.map((t) => t.id === id ? { ...t, claudeSessionId: sessionId } : t @@ -380,6 +452,9 @@ export const useTerminalStore = create((set, get) => ({ }, setClaudeBusy: (id: string, isBusy: boolean) => { + // Send CLAUDE_BUSY event to XState machine + sendTerminalMachineEvent(id, { type: 'CLAUDE_BUSY', isBusy }); + set((state) => ({ terminals: state.terminals.map((t) => t.id === id ? { ...t, isClaudeBusy: isBusy } : t @@ -388,6 +463,20 @@ export const useTerminalStore = create((set, get) => ({ }, setPendingClaudeResume: (id: string, pending: boolean) => { + // Send RESUME_REQUESTED or RESUME_COMPLETE to XState machine + if (pending) { + const terminal = get().terminals.find(t => t.id === id); + if (terminal?.claudeSessionId) { + sendTerminalMachineEvent(id, { type: 'RESUME_REQUESTED', claudeSessionId: terminal.claudeSessionId }); + } + } else { + // Resume cleared - either completed or cancelled + const actor = terminalActors.get(id); + if (actor && String(actor.getSnapshot().value) === 'pending_resume') { + sendTerminalMachineEvent(id, { type: 'RESUME_COMPLETE' }); + } + } + set((state) => ({ terminals: state.terminals.map((t) => t.id === id ? { ...t, pendingClaudeResume: pending } : t @@ -404,6 +493,11 @@ export const useTerminalStore = create((set, get) => ({ }, clearAllTerminals: () => { + // Clean up all XState actors + for (const [_id, actor] of terminalActors) { + actor.stop(); + } + terminalActors.clear(); set({ terminals: [], activeTerminalId: null, hasRestoredSessions: false }); },