mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-28 09:23:08 +08:00
147 lines
4.8 KiB
TypeScript
147 lines
4.8 KiB
TypeScript
// ========================================
|
|
// CLI Session Store (PTY-backed terminals)
|
|
// ========================================
|
|
// Zustand store for managing PTY session metadata and output chunks.
|
|
|
|
import { create } from 'zustand';
|
|
import { devtools } from 'zustand/middleware';
|
|
|
|
export interface CliSessionMeta {
|
|
sessionKey: string;
|
|
shellKind: string;
|
|
workingDir: string;
|
|
tool?: string;
|
|
model?: string;
|
|
resumeKey?: string;
|
|
createdAt: string;
|
|
updatedAt: string;
|
|
isPaused: boolean;
|
|
}
|
|
|
|
export interface CliSessionOutputChunk {
|
|
data: string;
|
|
timestamp: number;
|
|
}
|
|
|
|
interface CliSessionState {
|
|
sessions: Record<string, CliSessionMeta>;
|
|
outputChunks: Record<string, CliSessionOutputChunk[]>;
|
|
outputBytes: Record<string, number>;
|
|
|
|
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;
|
|
clearOutput: (sessionKey: string) => void;
|
|
}
|
|
|
|
const MAX_OUTPUT_BYTES_PER_SESSION = 2 * 1024 * 1024; // 2MB
|
|
|
|
const utf8Encoder = new TextEncoder();
|
|
function utf8ByteLength(value: string): number {
|
|
// Browser-safe alternative to Buffer.byteLength
|
|
return utf8Encoder.encode(value).length;
|
|
}
|
|
|
|
export const useCliSessionStore = create<CliSessionState>()(
|
|
devtools(
|
|
(set, get) => ({
|
|
sessions: {},
|
|
outputChunks: {},
|
|
outputBytes: {},
|
|
|
|
setSessions: (sessions) =>
|
|
set((state) => {
|
|
const nextSessions: Record<string, CliSessionMeta> = {};
|
|
for (const session of sessions) {
|
|
nextSessions[session.sessionKey] = session;
|
|
}
|
|
|
|
const keepKeys = new Set(Object.keys(nextSessions));
|
|
const nextChunks = { ...state.outputChunks };
|
|
const nextBytes = { ...state.outputBytes };
|
|
for (const key of Object.keys(nextChunks)) {
|
|
if (!keepKeys.has(key)) delete nextChunks[key];
|
|
}
|
|
for (const key of Object.keys(nextBytes)) {
|
|
if (!keepKeys.has(key)) delete nextBytes[key];
|
|
}
|
|
|
|
return { sessions: nextSessions, outputChunks: nextChunks, outputBytes: nextBytes };
|
|
}),
|
|
|
|
upsertSession: (session) =>
|
|
set((state) => ({
|
|
sessions: { ...state.sessions, [session.sessionKey]: session },
|
|
})),
|
|
|
|
removeSession: (sessionKey) =>
|
|
set((state) => {
|
|
const nextSessions = { ...state.sessions };
|
|
const nextChunks = { ...state.outputChunks };
|
|
const nextBytes = { ...state.outputBytes };
|
|
delete nextSessions[sessionKey];
|
|
delete nextChunks[sessionKey];
|
|
delete nextBytes[sessionKey];
|
|
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: {
|
|
...state.outputChunks,
|
|
[sessionKey]: buffer ? [{ data: buffer, timestamp: Date.now() }] : [],
|
|
},
|
|
outputBytes: {
|
|
...state.outputBytes,
|
|
[sessionKey]: buffer ? utf8ByteLength(buffer) : 0,
|
|
},
|
|
})),
|
|
|
|
appendOutput: (sessionKey, data, timestamp = Date.now()) => {
|
|
if (!data) return;
|
|
const chunkBytes = utf8ByteLength(data);
|
|
const { outputChunks, outputBytes } = get();
|
|
const existingChunks = outputChunks[sessionKey] ?? [];
|
|
const existingBytes = outputBytes[sessionKey] ?? 0;
|
|
|
|
const nextChunks = [...existingChunks, { data, timestamp }];
|
|
let nextBytes = existingBytes + chunkBytes;
|
|
|
|
// Ring-buffer by bytes
|
|
while (nextBytes > MAX_OUTPUT_BYTES_PER_SESSION && nextChunks.length > 0) {
|
|
const removed = nextChunks.shift();
|
|
if (removed) nextBytes -= utf8ByteLength(removed.data);
|
|
}
|
|
|
|
set((state) => ({
|
|
outputChunks: { ...state.outputChunks, [sessionKey]: nextChunks },
|
|
outputBytes: { ...state.outputBytes, [sessionKey]: Math.max(0, nextBytes) },
|
|
}));
|
|
},
|
|
|
|
clearOutput: (sessionKey) =>
|
|
set((state) => ({
|
|
outputChunks: { ...state.outputChunks, [sessionKey]: [] },
|
|
outputBytes: { ...state.outputBytes, [sessionKey]: 0 },
|
|
})),
|
|
}),
|
|
{ name: 'cliSessionStore' }
|
|
)
|
|
);
|