mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
feat: Add CLI session pause and resume functionality with UI integration
This commit is contained in:
@@ -15,6 +15,7 @@ export interface CliSessionMeta {
|
||||
resumeKey?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
isPaused: boolean;
|
||||
}
|
||||
|
||||
export interface CliSessionOutputChunk {
|
||||
@@ -30,6 +31,7 @@ interface CliSessionState {
|
||||
setSessions: (sessions: CliSessionMeta[]) => void;
|
||||
upsertSession: (session: CliSessionMeta) => void;
|
||||
removeSession: (sessionKey: string) => void;
|
||||
updateSessionPausedState: (sessionKey: string, isPaused: boolean) => void;
|
||||
|
||||
setBuffer: (sessionKey: string, buffer: string) => void;
|
||||
appendOutput: (sessionKey: string, data: string, timestamp?: number) => void;
|
||||
@@ -87,6 +89,18 @@ export const useCliSessionStore = create<CliSessionState>()(
|
||||
return { sessions: nextSessions, outputChunks: nextChunks, outputBytes: nextBytes };
|
||||
}),
|
||||
|
||||
updateSessionPausedState: (sessionKey, isPaused) =>
|
||||
set((state) => {
|
||||
const session = state.sessions[sessionKey];
|
||||
if (!session) return state;
|
||||
return {
|
||||
sessions: {
|
||||
...state.sessions,
|
||||
[sessionKey]: { ...session, isPaused, updatedAt: new Date().toISOString() },
|
||||
},
|
||||
};
|
||||
}),
|
||||
|
||||
setBuffer: (sessionKey, buffer) =>
|
||||
set((state) => ({
|
||||
outputChunks: {
|
||||
|
||||
@@ -17,6 +17,9 @@ import type {
|
||||
TerminalMeta,
|
||||
TerminalStatus,
|
||||
} from '../types/terminal-dashboard';
|
||||
import { pauseCliSession, resumeCliSession, closeCliSession, createCliSession } from '../lib/api';
|
||||
import { useCliSessionStore } from './cliSessionStore';
|
||||
import { useWorkflowStore, selectProjectPath } from './workflowStore';
|
||||
|
||||
// ========== Initial State ==========
|
||||
|
||||
@@ -178,6 +181,150 @@ export const useSessionManagerStore = create<SessionManagerStore>()(
|
||||
_workerRef.postMessage({ type: 'output', sessionId, text });
|
||||
}
|
||||
},
|
||||
|
||||
// ========== Session Lifecycle Actions ==========
|
||||
|
||||
pauseSession: async (terminalId: string) => {
|
||||
const projectPath = selectProjectPath(useWorkflowStore.getState());
|
||||
try {
|
||||
await pauseCliSession(terminalId, projectPath ?? undefined);
|
||||
set(
|
||||
(state) => {
|
||||
const existing = state.terminalMetas[terminalId];
|
||||
if (!existing) return state;
|
||||
return {
|
||||
terminalMetas: {
|
||||
...state.terminalMetas,
|
||||
[terminalId]: { ...existing, status: 'paused' as TerminalStatus },
|
||||
},
|
||||
};
|
||||
},
|
||||
false,
|
||||
'pauseSession'
|
||||
);
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('[SessionManager] pauseSession error:', error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
resumeSession: async (terminalId: string) => {
|
||||
const projectPath = selectProjectPath(useWorkflowStore.getState());
|
||||
// First update to 'resuming' status
|
||||
set(
|
||||
(state) => {
|
||||
const existing = state.terminalMetas[terminalId];
|
||||
if (!existing) return state;
|
||||
return {
|
||||
terminalMetas: {
|
||||
...state.terminalMetas,
|
||||
[terminalId]: { ...existing, status: 'resuming' as TerminalStatus },
|
||||
},
|
||||
};
|
||||
},
|
||||
false,
|
||||
'resumeSession/pending'
|
||||
);
|
||||
try {
|
||||
await resumeCliSession(terminalId, projectPath ?? undefined);
|
||||
// On success, update to 'active' status
|
||||
set(
|
||||
(state) => {
|
||||
const existing = state.terminalMetas[terminalId];
|
||||
if (!existing) return state;
|
||||
return {
|
||||
terminalMetas: {
|
||||
...state.terminalMetas,
|
||||
[terminalId]: { ...existing, status: 'active' as TerminalStatus },
|
||||
},
|
||||
};
|
||||
},
|
||||
false,
|
||||
'resumeSession/fulfilled'
|
||||
);
|
||||
} catch (error) {
|
||||
// On error, revert to 'paused' status
|
||||
set(
|
||||
(state) => {
|
||||
const existing = state.terminalMetas[terminalId];
|
||||
if (!existing) return state;
|
||||
return {
|
||||
terminalMetas: {
|
||||
...state.terminalMetas,
|
||||
[terminalId]: { ...existing, status: 'paused' as TerminalStatus },
|
||||
},
|
||||
};
|
||||
},
|
||||
false,
|
||||
'resumeSession/rejected'
|
||||
);
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('[SessionManager] resumeSession error:', error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
restartSession: async (terminalId: string) => {
|
||||
const projectPath = selectProjectPath(useWorkflowStore.getState());
|
||||
const cliStore = useCliSessionStore.getState();
|
||||
const session = cliStore.sessions[terminalId];
|
||||
|
||||
if (!session) {
|
||||
throw new Error(`Session not found: ${terminalId}`);
|
||||
}
|
||||
|
||||
// Store session config for recreation
|
||||
const sessionConfig = {
|
||||
workingDir: session.workingDir,
|
||||
tool: session.tool,
|
||||
model: session.model,
|
||||
resumeKey: session.resumeKey,
|
||||
shellKind: session.shellKind,
|
||||
};
|
||||
|
||||
try {
|
||||
// Close existing session
|
||||
await closeCliSession(terminalId, projectPath ?? undefined);
|
||||
|
||||
// Create new session with same config
|
||||
const result = await createCliSession(
|
||||
{
|
||||
workingDir: sessionConfig.workingDir,
|
||||
preferredShell: sessionConfig.shellKind === 'powershell' ? 'pwsh' : 'bash',
|
||||
tool: sessionConfig.tool,
|
||||
model: sessionConfig.model,
|
||||
resumeKey: sessionConfig.resumeKey,
|
||||
},
|
||||
projectPath ?? undefined
|
||||
);
|
||||
|
||||
// Update terminal meta to active status
|
||||
set(
|
||||
(state) => {
|
||||
const existing = state.terminalMetas[terminalId];
|
||||
if (!existing) return state;
|
||||
return {
|
||||
terminalMetas: {
|
||||
...state.terminalMetas,
|
||||
[terminalId]: { ...existing, status: 'active' as TerminalStatus, alertCount: 0 },
|
||||
},
|
||||
};
|
||||
},
|
||||
false,
|
||||
'restartSession'
|
||||
);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (import.meta.env.DEV) {
|
||||
console.error('[SessionManager] restartSession error:', error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
}),
|
||||
{ name: 'SessionManagerStore' }
|
||||
)
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
removePaneFromLayout,
|
||||
getAllPaneIds,
|
||||
} from '@/lib/layout-utils';
|
||||
import type { CreateCliSessionInput, CliSession } from '@/lib/api';
|
||||
import { createCliSession } from '@/lib/api';
|
||||
|
||||
// ========== Types ==========
|
||||
|
||||
@@ -37,6 +39,12 @@ export interface TerminalGridActions {
|
||||
assignSession: (paneId: PaneId, sessionId: string | null) => void;
|
||||
setFocused: (paneId: PaneId) => void;
|
||||
resetLayout: (preset: 'single' | 'split-h' | 'split-v' | 'grid-2x2') => void;
|
||||
/** Create a new CLI session and assign it to a new pane (auto-split from specified pane) */
|
||||
createSessionAndAssign: (
|
||||
paneId: PaneId,
|
||||
config: CreateCliSessionInput,
|
||||
projectPath: string | null
|
||||
) => Promise<{ paneId: PaneId; session: CliSession } | null>;
|
||||
}
|
||||
|
||||
export type TerminalGridStore = TerminalGridState & TerminalGridActions;
|
||||
@@ -231,6 +239,60 @@ export const useTerminalGridStore = create<TerminalGridStore>()(
|
||||
'terminalGrid/resetLayout'
|
||||
);
|
||||
},
|
||||
|
||||
createSessionAndAssign: async (paneId, config, projectPath) => {
|
||||
try {
|
||||
// 1. Create the CLI session via API
|
||||
const result = await createCliSession(config, projectPath ?? undefined);
|
||||
const session = result.session;
|
||||
|
||||
// 2. Get current state
|
||||
const state = get();
|
||||
|
||||
// 3. Check if the current pane is empty (no session assigned)
|
||||
const currentPane = state.panes[paneId];
|
||||
const isCurrentPaneEmpty = !currentPane?.sessionId;
|
||||
|
||||
if (isCurrentPaneEmpty) {
|
||||
// Assign session to current empty pane
|
||||
set(
|
||||
{
|
||||
panes: {
|
||||
...state.panes,
|
||||
[paneId]: { ...currentPane, sessionId: session.sessionKey },
|
||||
},
|
||||
focusedPaneId: paneId,
|
||||
},
|
||||
false,
|
||||
'terminalGrid/createSessionAndAssign'
|
||||
);
|
||||
return { paneId, session };
|
||||
}
|
||||
|
||||
// 4. Current pane has session, auto-split and assign to new pane
|
||||
const newPaneId = generatePaneId(state.nextPaneIdCounter);
|
||||
const newLayout = addPaneToLayout(state.layout, newPaneId, paneId, 'horizontal');
|
||||
|
||||
set(
|
||||
{
|
||||
layout: newLayout,
|
||||
panes: {
|
||||
...state.panes,
|
||||
[newPaneId]: { id: newPaneId, sessionId: session.sessionKey },
|
||||
},
|
||||
focusedPaneId: newPaneId,
|
||||
nextPaneIdCounter: state.nextPaneIdCounter + 1,
|
||||
},
|
||||
false,
|
||||
'terminalGrid/createSessionAndAssign'
|
||||
);
|
||||
|
||||
return { paneId: newPaneId, session };
|
||||
} catch (error) {
|
||||
console.error('Failed to create CLI session:', error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
}),
|
||||
{ name: 'TerminalGridStore' }
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user