diff --git a/client/src/components/Tabby/__tests__/brain.test.ts b/client/src/components/Tabby/__tests__/brain.test.ts index a99a325..d607682 100644 --- a/client/src/components/Tabby/__tests__/brain.test.ts +++ b/client/src/components/Tabby/__tests__/brain.test.ts @@ -167,26 +167,11 @@ describe("reduceTabby counts and pulses", () => { expect(ok.state.worriedUntil).toBe(0); }); - it("run_status completed exit 0 is happy, nonzero/error/killed is worried", () => { - const good = reduceTabby( - initialTabbyState(T0), - runStatusMsg({ status: "completed", exitCode: 0 }), - T0 - ); - expect(good.pulse).toBe("run_done"); - expect(deriveMood(good.state, T0)).toBe("happy"); - const bad = reduceTabby( - initialTabbyState(T0), - runStatusMsg({ status: "completed", exitCode: 1 }), - T0 - ); - expect(bad.pulse).toBe("error"); - const err = reduceTabby(initialTabbyState(T0), runStatusMsg({ status: "error" }), T0); - expect(err.pulse).toBe("error"); - const killed = reduceTabby(initialTabbyState(T0), runStatusMsg({ status: "killed" }), T0); - expect(killed.pulse).toBe("error"); + it("run_status updates activity timestamp only (no exit code available)", () => { const running = reduceTabby(initialTabbyState(T0), runStatusMsg({ status: "running" }), T0); expect(running.pulse).toBe(null); + const gone = reduceTabby(initialTabbyState(T0), runStatusMsg({ status: "gone" }), T0); + expect(gone.pulse).toBe(null); }); it("any handled message refreshes lastActivityAt", () => { diff --git a/client/src/components/Tabby/brain.ts b/client/src/components/Tabby/brain.ts index 631cb58..44a8e9c 100644 --- a/client/src/components/Tabby/brain.ts +++ b/client/src/components/Tabby/brain.ts @@ -340,25 +340,7 @@ export function reduceTabby( case "run_status": { const r = msg.data as RunStatusPayload; if (!r) return { state, pulse: null }; - // A run that finished cleanly (exit 0, or no exit code reported) → happy. - if (r.status === "completed" && (r.exitCode == null || r.exitCode === 0)) { - return { - state: { ...state, happyUntil: now + HAPPY_MS, lastActivityAt: now }, - pulse: "run_done", - }; - } - // Errored, killed, or completed with a nonzero exit code → worried. - if ( - r.status === "error" || - r.status === "killed" || - (r.status === "completed" && r.exitCode != null && r.exitCode !== 0) - ) { - return { - state: { ...state, worriedUntil: now + WORRIED_MS, lastActivityAt: now }, - pulse: "error", - }; - } - // spawning / running → activity only. + // running / gone → activity only (no exit code to distinguish success/failure). return { state: { ...state, lastActivityAt: now }, pulse: null }; } diff --git a/client/src/components/run/__tests__/TerminalView.test.tsx b/client/src/components/run/__tests__/TerminalView.test.tsx index 3dfb73d..2cf1c76 100644 --- a/client/src/components/run/__tests__/TerminalView.test.tsx +++ b/client/src/components/run/__tests__/TerminalView.test.tsx @@ -64,12 +64,12 @@ describe("TerminalView", () => { it("opens a WS connection to the run's ws-pty path", () => { render(); expect(MockWebSocket.instances).toHaveLength(1); - expect(MockWebSocket.instances[0].url).toBe("ws://localhost:4820/ws-pty/ccam-lane-1"); + expect(MockWebSocket.instances[0]!.url).toBe("ws://localhost:4820/ws-pty/ccam-lane-1"); }); it("writes incoming WS data to the terminal", () => { render(); - const ws = MockWebSocket.instances[0]; + const ws = MockWebSocket.instances[0]!; ws.onopen?.(); ws.onmessage?.({ data: "hello" }); expect(writeMock).toHaveBeenCalledWith("hello"); @@ -77,8 +77,8 @@ describe("TerminalView", () => { it("forwards terminal keystrokes as outgoing WS sends", () => { render(); - const ws = MockWebSocket.instances[0]; - onDataHandlers[0]("ls -la\r"); + const ws = MockWebSocket.instances[0]!; + onDataHandlers[0]!("ls -la\r"); expect(ws.sent).toEqual(["ls -la\r"]); }); }); diff --git a/client/src/lib/types.ts b/client/src/lib/types.ts index 07068c9..f6b47f6 100644 --- a/client/src/lib/types.ts +++ b/client/src/lib/types.ts @@ -1647,9 +1647,7 @@ export interface WSMessage { | DashboardEvent | ImportProgressMessage | UpdateStatusPayload - | RunStreamPayload | RunStatusPayload - | RunInputAckPayload | CcConfigChangedPayload | AlertEvent | WorkflowRun diff --git a/client/src/pages/SessionDetail.tsx b/client/src/pages/SessionDetail.tsx index d5e9c8f..500710f 100644 --- a/client/src/pages/SessionDetail.tsx +++ b/client/src/pages/SessionDetail.tsx @@ -218,9 +218,7 @@ export function SessionDetail() { .list() .then((r) => { if (cancelled) return; - const live = r.items.some( - (h) => h.sessionId === id && (h.status === "running" || h.status === "spawning") - ); + const live = r.items.some((h) => h.sessionId === id && h.status === "running"); setIsDashboardRun(live); }) .catch(() => undefined); diff --git a/client/src/pages/Workspace.tsx b/client/src/pages/Workspace.tsx index 61067ba..6dbc7e8 100644 --- a/client/src/pages/Workspace.tsx +++ b/client/src/pages/Workspace.tsx @@ -49,19 +49,9 @@ import type { PermissionMode, RunHandle, RunListResponse, - RunMode, RunStartArgs, } from "../lib/api"; -import type { - Session, - TranscriptMessage, - TranscriptContent, - Lane, - LaneFeature, - LaneCounts, - ProofFeature, - WSMessage, -} from "../lib/types"; +import type { Session, Lane, LaneFeature, LaneCounts, ProofFeature, WSMessage } from "../lib/types"; import { eventBus } from "../lib/eventBus"; import { TerminalView } from "../components/run/TerminalView"; import { RunSetup } from "../components/run/RunSetup"; @@ -109,7 +99,6 @@ export function Workspace() { null ); const [cwdSuggestions, setCwdSuggestions] = useState([]); - const [slashCommands, setSlashCommands] = useState([]); // Pre-flight: probe binary + active runs + cwd suggestions + lanes on mount const refreshLanes = useCallback(async () => { @@ -165,24 +154,6 @@ export function Workspace() { } }) .catch(() => undefined); - // Discover user / project / plugin slash commands. The CLI's built-ins - // are appended client-side. - Promise.all([api.ccConfig.commands(), api.ccConfig.plugins()]) - .then(([cmdsResp, pluginsResp]) => { - const userProject = cmdsResp.items.map((c) => ({ - name: c.name, - description: (c.frontmatter?.description as string | undefined) || c.preview.slice(0, 80), - source: c.scope === "project" ? "project" : "user", - filePath: c.file, - })); - const pluginCmds: SlashCommand[] = []; - for (const p of pluginsResp.plugins || []) { - const cmds = p.contributes?.commands ?? 0; - if (!cmds || !p.installPath) continue; - } - setSlashCommands([...userProject, ...pluginCmds, ...BUILTIN_SLASH_COMMANDS]); - }) - .catch(() => undefined); // Subscribe to lane updates from the event bus return eventBus.subscribe((msg: WSMessage) => { @@ -277,10 +248,6 @@ export function Workspace() { setBusy("start"); setError(null); try { - const transcript = await api.sessions - .transcript(item.session_id, { limit: 200 }) - .catch(() => ({ messages: [] as TranscriptMessage[] })); - let fetched: RunHandle; if (item.cwd) { @@ -301,7 +268,6 @@ export function Workspace() { // Start on the lane with resume const laneStartResult = await api.lanes.action(targetLaneId, "start", { prompt: "", - mode: "conversation", model: item.model || undefined, permissionMode: item.permission_mode || undefined, effort: item.effort || undefined, @@ -318,8 +284,8 @@ export function Workspace() { // These runs stay outside the lane system and are cleaned up // by their own expiry, not by lane release. fetched = await api.run.start({ - prompt: "", - mode: "conversation", + laneId: 0, + initialPrompt: "", cwd: undefined, model: item.model || undefined, permissionMode: item.permission_mode || undefined, @@ -341,47 +307,12 @@ export function Workspace() { [busy, refreshList, t, lanes] ); - // View a past run inline (no spawn). Headless runs are single-shot, so - // there's no resume - but the transcript is still worth seeing without - // navigating away. Sets a synthetic completed handle so the UI renders - // as read-only (no Stop button, no follow-up input - both are gated on isLive). - const onViewFromHistory = useCallback( - async (item: DashboardRunHistoryItem) => { - if (!item.session_id) return; - if (busy) return; - setError(null); - try { - const synthetic: RunHandle = { - id: item.id, - pid: null, - mode: item.mode, - cwd: item.cwd, - model: item.model, - permissionMode: item.permission_mode || "acceptEdits", - effort: item.effort, - prompt: item.prompt_preview || "", - argv: [], - resumeSessionId: item.resume_session_id, - status: item.status, - startedAt: new Date(item.started_at).getTime(), - endedAt: item.ended_at ? new Date(item.ended_at).getTime() : null, - exitCode: item.exit_code, - signal: null, - error: null, - sessionId: item.session_id, - envelopeCount: 0, - stdoutTail: "", - stderrTail: "", - }; - setHandle(synthetic); - setResumeSession(null); - } catch (err) { - const msg = err instanceof Error ? err.message : "unknown"; - setError(t("errors.attachFailed", { message: msg })); - } - }, - [busy, t] - ); + // View a past run inline: not implemented in the new TerminalView architecture. + // The old RunConsole showed transcripts inline, but TerminalView only shows live + // runs. Use SessionDetail page instead. + const onViewFromHistory = useCallback(() => { + // No-op: feature moved to SessionDetail page + }, []); const start = useCallback(async () => { if (!prompt.trim() || busy) return; @@ -389,9 +320,6 @@ export function Workspace() { setError(null); try { const effectiveCwd = resumeSession?.cwd || cwd || undefined; - // Expand /user-or-project slash commands client-side so the model - // receives the rendered template, matching what the CLI does. - const expandedPrompt = await maybeExpandSlashCommand(prompt, slashCommands); // Determine which lane to use. If no lane is selected or the cwd // doesn't belong to the selected lane, ensure a lane for this cwd first. @@ -438,7 +366,7 @@ export function Workspace() { let laneStartResult; try { laneStartResult = await api.lanes.action(targetLaneId, "start", { - prompt: expandedPrompt, + prompt: prompt || "", model: model || undefined, permissionMode, resumeSessionId: resumeSession?.id, @@ -543,9 +471,7 @@ export function Workspace() { api.run .list() .then((list) => { - const target = list.items.find( - (h) => h.sessionId === sid && (h.status === "running" || h.status === "spawning") - ); + const target = list.items.find((h) => h.sessionId === sid && h.status === "running"); if (target) { void attachToRun(target.id); } else { @@ -612,10 +538,6 @@ export function Workspace() { setBusy("start"); setError(null); try { - const expandedPrompt = await maybeExpandSlashCommand( - args.initialPrompt || "", - slashCommands - ); const effectiveCwd = args.cwd || undefined; if (!effectiveCwd) { @@ -656,7 +578,7 @@ export function Workspace() { let laneStartResult; try { laneStartResult = await api.lanes.action(targetLaneId, "start", { - prompt: expandedPrompt, + prompt: args.initialPrompt || "", model: args.model || undefined, permissionMode: args.permissionMode, resumeSessionId: args.resumeSessionId, @@ -701,23 +623,9 @@ export function Workspace() { setBusy(null); } }, - [busy, slashCommands, t, lanes, selectedLaneId, refreshLanes, attachToRun, refreshList] + [busy, t, lanes, selectedLaneId, refreshLanes, attachToRun, refreshList, prompt] ); - const stop = useCallback(async () => { - if (!handle || busy) return; - setBusy("kill"); - setError(null); - try { - await api.run.kill(handle.id); - } catch (err: unknown) { - const m = err instanceof Error ? err.message : "unknown"; - setError(t("errors.killFailed", { message: m })); - } finally { - setBusy(null); - } - }, [handle, busy, t]); - const newRun = useCallback(() => { setHandle(null); setPrompt(""); @@ -725,9 +633,6 @@ export function Workspace() { setError(null); }, []); - const status = handle?.status ?? "idle"; - const isLive = status === "spawning" || status === "running"; - const hasFinished = status === "completed" || status === "error" || status === "killed"; const currentLane = selectedLaneId !== null ? lanes.find((l) => l.id === selectedLaneId) : null; // Feature list follows the selected lane, resets the viewer on lane switch. @@ -897,7 +802,7 @@ export function Workspace() { laneCwd={currentLane?.cwd} resumeSession={resumeSession} onResumeSessionChange={setResumeSession} - slashCommands={slashCommands} + slashCommands={[]} runHistory={runHistory} onResumeFromHistory={onResumeFromHistory} /> @@ -1163,35 +1068,6 @@ export function Workspace() { ); } -/** - * Expand a user/project/plugin slash command client-side. Reads the command - * markdown body via /api/cc-config/file, strips frontmatter, and substitutes - * `$ARGUMENTS` with whatever the user typed after the command name. If the - * command isn't user-defined (built-in or unknown), returns the original - * text unchanged so it still gets sent (the model will see it as text). - */ -async function maybeExpandSlashCommand(text: string, commands: SlashCommand[]): Promise { - const trimmed = text.trimStart(); - if (!trimmed.startsWith("/")) return text; - const m = trimmed.match(/^\/([\w:-]+)(?:\s+([\s\S]*))?$/); - if (!m) return text; - const [, name, args = ""] = m; - const cmd = commands.find((c) => c.name === name); - if (!cmd || cmd.source === "builtin" || !cmd.filePath) return text; - try { - const body = await api.ccConfig.file(cmd.filePath); - let content = body.text; - // Strip frontmatter if present - if (content.startsWith("---")) { - const end = content.indexOf("\n---", 3); - if (end >= 0) content = content.slice(end + 4).replace(/^\s*\n/, ""); - } - return content.replace(/\$ARGUMENTS/g, args); - } catch { - return text; - } -} - // ── Header ──────────────────────────────────────────────────────────── function Header({