feat(run): wire Workspace to TerminalView, delete the stream-json Run feature
Combines three tasks that couldn't land as separate commits: the pre-commit hook's full test run crashes on any intermediate state where Workspace.tsx still imports the files being deleted, so the deletion (old RunConsole/useRunStream/run-spawner/stream-json-parser), the RunSetup/RunHistory type adjustments, and this file's own TerminalView wiring had to be staged together and committed as one hook-passable unit. - Delete RunConsole.tsx, useRunStream.ts, server/lib/run-spawner.js, server/lib/stream-json-parser.js and their tests (Task 8). - Adjust RunSetup.tsx/RunHistory.tsx to the tmux-backed RunHandle/ RunStartArgs/DashboardRunHistoryItem shapes, remove mode selection UI (Task 9). - Swap Workspace.tsx's chat-bubble run console for TerminalView (xterm.js over /ws-pty/:runId), drop the stream-json envelope plumbing, update Start/Resume to the new RunStartArgs payload. Create onStartFromSetup handler to work with RunSetup's new callback shape. Remove mode state and related plumbing. Remove send/followUp state (no longer using old RunConsole chat interface). - Add promptPlaceholderTerminal i18n key to support RunSetup's new placeholder text (Task 10). - Update Workspace.test.tsx to mock TerminalView component. - Regenerate screens.snapshot.test.tsx snapshot (only Workspace run panel changes: terminal container instead of chat bubbles).
This commit is contained in:
@@ -33,8 +33,27 @@ import {
|
||||
RotateCcw,
|
||||
Eye,
|
||||
} from "lucide-react";
|
||||
import type { DashboardRunHistoryItem, RunListResponse, RunMode, RunStatus } from "../../lib/api";
|
||||
import { ModeBadge, StatusPill } from "./RunConsole";
|
||||
import type { DashboardRunHistoryItem, RunListResponse, RunStatus } from "../../lib/api";
|
||||
|
||||
// Minimal StatusPill component (from deleted RunConsole)
|
||||
function StatusPill({
|
||||
status,
|
||||
}: {
|
||||
status: RunStatus | "completed" | "error" | "killed" | "abandoned";
|
||||
}) {
|
||||
const colors: Record<string, string> = {
|
||||
running: "bg-status-success/10 text-status-success border-status-success/30",
|
||||
gone: "bg-surface-3 text-fg-secondary border-border",
|
||||
completed: "bg-sky-500/10 text-sky-300 border-sky-500/30",
|
||||
error: "bg-status-danger/10 text-status-danger border-status-danger/30",
|
||||
killed: "bg-surface-3 text-fg-secondary border-border",
|
||||
abandoned: "bg-surface-3 text-fg-secondary border-border",
|
||||
};
|
||||
const color = colors[status] || colors.abandoned;
|
||||
return (
|
||||
<span className={`text-[10px] font-mono px-1.5 py-0.5 rounded border ${color}`}>{status}</span>
|
||||
);
|
||||
}
|
||||
|
||||
type RunStatusFilter =
|
||||
| "all"
|
||||
@@ -44,15 +63,13 @@ type RunStatusFilter =
|
||||
| "error"
|
||||
| "killed"
|
||||
| "abandoned";
|
||||
type RunModeFilter = "all" | "conversation" | "headless";
|
||||
|
||||
export interface UnifiedRunRow {
|
||||
id: string;
|
||||
sessionId: string | null;
|
||||
mode: RunMode;
|
||||
cwd: string;
|
||||
model: string | null;
|
||||
status: RunStatus;
|
||||
status: RunStatus | "completed" | "error" | "killed" | "abandoned";
|
||||
promptPreview: string;
|
||||
startedAt: number;
|
||||
endedAt: number | null;
|
||||
@@ -105,14 +122,13 @@ export function ActiveRunsSwitcher({
|
||||
out.push({
|
||||
id: r.id,
|
||||
sessionId: r.sessionId,
|
||||
mode: r.mode,
|
||||
cwd: r.cwd,
|
||||
cwd: r.cwd || "",
|
||||
model: r.model,
|
||||
status: r.status,
|
||||
promptPreview: r.prompt || "",
|
||||
startedAt: r.startedAt,
|
||||
endedAt: r.endedAt,
|
||||
isLive: r.status === "running" || r.status === "spawning",
|
||||
promptPreview: "",
|
||||
startedAt: r.startedAt ? new Date(r.startedAt).getTime() : 0,
|
||||
endedAt: null,
|
||||
isLive: r.status === "running",
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -124,7 +140,6 @@ export function ActiveRunsSwitcher({
|
||||
out.push({
|
||||
id: h.id,
|
||||
sessionId: h.session_id,
|
||||
mode: h.mode,
|
||||
cwd: h.cwd,
|
||||
model: h.model,
|
||||
status: h.status,
|
||||
@@ -138,7 +153,7 @@ export function ActiveRunsSwitcher({
|
||||
return out;
|
||||
}, [activeRuns, runHistory]);
|
||||
|
||||
const liveCount = activeRuns?.activeCount ?? 0;
|
||||
const liveCount = rows.filter((r) => r.isLive).length;
|
||||
const totalCount = rows.length;
|
||||
|
||||
return (
|
||||
@@ -211,7 +226,6 @@ export function RunsModal({
|
||||
}) {
|
||||
const { t } = useTranslation("run");
|
||||
const [statusFilter, setStatusFilter] = useState<RunStatusFilter>("all");
|
||||
const [modeFilter, setModeFilter] = useState<RunModeFilter>("all");
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
// Snappy refresh while the modal is the foreground UI: pull immediately
|
||||
@@ -227,25 +241,22 @@ export function RunsModal({
|
||||
|
||||
const counts = useMemo(() => {
|
||||
const byStatus: Record<string, number> = { all: rows.length };
|
||||
const byMode: Record<string, number> = { all: rows.length };
|
||||
for (const r of rows) {
|
||||
byStatus[r.status] = (byStatus[r.status] || 0) + 1;
|
||||
byMode[r.mode] = (byMode[r.mode] || 0) + 1;
|
||||
}
|
||||
return { byStatus, byMode };
|
||||
return { byStatus };
|
||||
}, [rows]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = search.trim().toLowerCase();
|
||||
return rows.filter((r) => {
|
||||
if (statusFilter !== "all" && r.status !== statusFilter) return false;
|
||||
if (modeFilter !== "all" && r.mode !== modeFilter) return false;
|
||||
if (!q) return true;
|
||||
const hay =
|
||||
r.promptPreview + "\n" + r.cwd + "\n" + (r.sessionId || "") + "\n" + (r.model || "");
|
||||
return hay.toLowerCase().includes(q);
|
||||
});
|
||||
}, [rows, statusFilter, modeFilter, search]);
|
||||
}, [rows, statusFilter, search]);
|
||||
|
||||
const historyById = useMemo(() => {
|
||||
const m = new Map<string, DashboardRunHistoryItem>();
|
||||
@@ -261,7 +272,6 @@ export function RunsModal({
|
||||
"killed",
|
||||
"abandoned",
|
||||
];
|
||||
const MODES: RunModeFilter[] = ["all", "conversation", "headless"];
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -343,16 +353,6 @@ export function RunsModal({
|
||||
}))}
|
||||
onChange={(v) => setStatusFilter(v as RunStatusFilter)}
|
||||
/>
|
||||
<FilterChipGroup
|
||||
label={t("runs.filterMode", "Mode")}
|
||||
value={modeFilter}
|
||||
options={MODES.map((m) => ({
|
||||
value: m,
|
||||
label: m === "all" ? t("runs.allLabel", "All") : t(`mode.${m}`),
|
||||
count: counts.byMode[m] || 0,
|
||||
}))}
|
||||
onChange={(v) => setModeFilter(v as RunModeFilter)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -467,10 +467,9 @@ function UnifiedRunRowView({
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
const canResume = row.mode === "conversation" && !!row.sessionId && !row.isLive;
|
||||
// Headless runs are single-shot, so resume doesn't apply - but the captured
|
||||
// transcript is still worth viewing. Link to the Session detail page.
|
||||
const canView = row.mode === "headless" && !!row.sessionId && !row.isLive;
|
||||
// Without mode distinction, offer resume for any finished run with a session
|
||||
const canResume = !!row.sessionId && !row.isLive;
|
||||
const canView = !!row.sessionId && !row.isLive;
|
||||
return (
|
||||
<div
|
||||
className={`px-5 py-3 transition-colors ${
|
||||
@@ -479,7 +478,6 @@ function UnifiedRunRowView({
|
||||
>
|
||||
<div className="flex items-center gap-2 mb-1.5 flex-wrap">
|
||||
<StatusPill status={row.status} />
|
||||
<ModeBadge mode={row.mode} />
|
||||
{row.isLive && (
|
||||
<span className="text-[10px] font-semibold text-status-success bg-status-success/10 border border-status-success/25 px-1.5 py-0.5 rounded-full inline-flex items-center gap-1">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse" />
|
||||
|
||||
Reference in New Issue
Block a user