fix(client): clean up leftover pre-TerminalView dead code and type errors in Workspace.tsx

`npm run test:client` (Vitest/esbuild) doesn't type-check, so several
tasks' incomplete cleanup of the old RunConsole-era code in
Workspace.tsx (SlashCommand/BUILTIN_SLASH_COMMANDS references, old
RunStatus values, a stray `mode` field, a wrong `prompt` vs
`initialPrompt` key) went unnoticed through every review until `tsc
--noEmit` was run directly. Also fixes a stale `RunStatusPayload.exitCode`
read in Tabby's brain.ts and dangling RunStreamPayload/RunInputAckPayload
references left in types.ts.
This commit is contained in:
2026-08-12 16:09:43 +07:00
parent 9c3331c843
commit cb8800fa31
6 changed files with 23 additions and 184 deletions
@@ -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", () => {
+1 -19
View File
@@ -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 };
}
@@ -64,12 +64,12 @@ describe("TerminalView", () => {
it("opens a WS connection to the run's ws-pty path", () => {
render(<TerminalView runId="ccam-lane-1" wsBaseUrl="ws://localhost:4820" />);
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(<TerminalView runId="ccam-lane-1" wsBaseUrl="ws://localhost:4820" />);
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(<TerminalView runId="ccam-lane-1" wsBaseUrl="ws://localhost:4820" />);
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"]);
});
});
-2
View File
@@ -1647,9 +1647,7 @@ export interface WSMessage {
| DashboardEvent
| ImportProgressMessage
| UpdateStatusPayload
| RunStreamPayload
| RunStatusPayload
| RunInputAckPayload
| CcConfigChangedPayload
| AlertEvent
| WorkflowRun
+1 -3
View File
@@ -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);
+14 -138
View File
@@ -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<CwdSuggestion[]>([]);
const [slashCommands, setSlashCommands] = useState<any[]>([]);
// 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<SlashCommand>((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<string> {
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({