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({