This commit is contained in:
2026-02-19 21:48:24 +00:00
42 changed files with 1674 additions and 374 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ export type SessionEntry = {
id: string;
title: string;
createdAt: number;
provider: 'claude' | 'opencode';
provider: 'claude' | 'opencode' | 'pi-mono';
model?: string | null;
};
@@ -3,7 +3,7 @@ import { ArrowLeft, Archive, Trash2, Maximize2, Minimize2 } from 'lucide-react';
type SessionBarProps = {
listPath: string;
provider: 'claude' | 'opencode';
provider: 'claude' | 'opencode' | 'pi-mono';
sessionTitle: string | undefined;
isConnected: boolean;
isGenerating: boolean;
+36
View File
@@ -26,6 +26,7 @@ export type TerminalViewProps = {
autoFocus?: boolean;
onReady?: (term: XTerm) => void;
onExit?: () => void;
onCommandDone?: (exitCode: number, output: string) => void;
onDisconnect?: () => void;
};
@@ -62,6 +63,7 @@ export const TerminalView = ({
autoFocus = true,
onReady,
onExit,
onCommandDone,
onDisconnect,
}: TerminalViewProps) => {
const containerRef = useRef<HTMLDivElement>(null);
@@ -71,11 +73,15 @@ export const TerminalView = ({
const isMounted = useMounted();
const onReadyRef = useRef<TerminalViewProps['onReady']>(onReady);
const onExitRef = useRef<TerminalViewProps['onExit']>(onExit);
const onCommandDoneRef = useRef<TerminalViewProps['onCommandDone']>(onCommandDone);
const onDisconnectRef = useRef<TerminalViewProps['onDisconnect']>(onDisconnect);
const commandRef = useRef(command);
onReadyRef.current = onReady;
onExitRef.current = onExit;
onCommandDoneRef.current = onCommandDone;
onDisconnectRef.current = onDisconnect;
commandRef.current = command;
const background = theme?.background ?? DEFAULT_THEME.background;
const foreground = theme?.foreground ?? DEFAULT_THEME.foreground;
@@ -121,6 +127,12 @@ export const TerminalView = ({
const ws = new WebSocket(buildWsUrl(wsPath, sessionId, sandboxed, cwd, command));
wsRef.current = ws;
let commandSent = false;
let commandDone = false;
let commandOutput = '';
const EXIT_MARKER = '__OFFICER_EXIT_';
// eslint-disable-next-line no-control-regex
const stripAnsi = (s: string) => s.replace(/\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07/g, '');
const handleOpen = () => {
ws.send(JSON.stringify({ type: 'resize', cols: term.cols, rows: term.rows }));
@@ -131,6 +143,30 @@ export const TerminalView = ({
const msg = JSON.parse(ev.data as string);
if (msg.type === 'output') {
term.write(msg.data);
if (commandRef.current && !commandSent) {
commandSent = true;
setTimeout(() => {
if (ws.readyState === WebSocket.OPEN) {
const wrapped = onCommandDoneRef.current
? `${commandRef.current}; echo "${EXIT_MARKER}$?__"`
: commandRef.current;
ws.send(JSON.stringify({ type: 'input', data: wrapped + '\r' }));
}
}, 100);
}
if (commandSent && !commandDone && onCommandDoneRef.current) {
commandOutput += msg.data as string;
const markerMatch = stripAnsi(commandOutput).match(/__OFFICER_EXIT_(\d+)__/);
if (markerMatch) {
const exitCode = Number(markerMatch[1]);
const raw = stripAnsi(commandOutput).slice(0, markerMatch.index);
const lines = raw.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
const cmdLine = lines.findIndex((l) => l.includes(commandRef.current!.slice(0, 20)));
const output = lines.slice(cmdLine >= 0 ? cmdLine + 1 : 0).join('\n').trim();
commandDone = true;
onCommandDoneRef.current(exitCode, output);
}
}
} else if (msg.type === 'exit') {
term.write('\r\n[Process exited]\r\n');
onExitRef.current?.();