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 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
6c991774ad
commit
1302c61b71
@@ -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<typeof terminalMachine>;
|
||||
|
||||
/**
|
||||
* 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<string, TerminalActor>();
|
||||
|
||||
/**
|
||||
* 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<typeof terminalMachine>): {
|
||||
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<TerminalState>((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<TerminalState>((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<TerminalState>((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<TerminalState>((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<TerminalState>((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<TerminalState>((set, get) => ({
|
||||
},
|
||||
|
||||
clearAllTerminals: () => {
|
||||
// Clean up all XState actors
|
||||
for (const [_id, actor] of terminalActors) {
|
||||
actor.stop();
|
||||
}
|
||||
terminalActors.clear();
|
||||
set({ terminals: [], activeTerminalId: null, hasRestoredSessions: false });
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user