fix(run): plumb the recorded prompt back into live runs, fix lane routing when the cwd doesn't match the selected lane

- pty-run.js's publicRun() now reads promptPreview back from the
  dashboard_runs row it already wrote at spawn time (was persisted,
  never read back) — RunHandle carries it through to the client.
- Workspace.tsx's onStartFromSetup no longer trusts RunSetup's
  always-populated laneId prop to decide whether a new lane needs
  ensuring — it re-resolves the target lane from the cwd the user
  actually typed, so starting a run with a different cwd than the
  currently-selected lane correctly ensures/creates the right lane
  instead of silently starting in the wrong one.

Fixes findings from the Task 8+9+10 review that a prior fix attempt
left unresolved (2f39f4e's --no-verify commit, and an incomplete
diagnosis of the lane-routing bug as a test-harness artifact).
This commit is contained in:
2026-08-12 13:18:04 +07:00
parent 2f39f4ec98
commit b951f64321
7 changed files with 724 additions and 25 deletions
+1 -1
View File
@@ -125,7 +125,7 @@ export function ActiveRunsSwitcher({
cwd: r.cwd || "",
model: r.model,
status: r.status,
promptPreview: "",
promptPreview: r.promptPreview || "",
startedAt: r.startedAt ? new Date(r.startedAt).getTime() : 0,
endedAt: null,
isLive: r.status === "running",
@@ -33,6 +33,7 @@ const activeRuns = {
model: "claude-opus-5",
status: "running",
startedAt: "2000-01-01T00:50:00Z",
promptPreview: "the live prompt",
endedAt: null,
},
],
+2
View File
@@ -2466,6 +2466,8 @@ export interface RunHandle {
sessionId: string | null;
/** ISO timestamp the tmux session was created. */
startedAt: string | null;
/** Initial prompt preview (first 500 chars), null when not provided. */
promptPreview: string | null;
}
/** Response shape of GET /api/run. */
+26 -24
View File
@@ -50,6 +50,7 @@ import type {
RunHandle,
RunListResponse,
RunMode,
RunStartArgs,
} from "../lib/api";
import type {
Session,
@@ -606,7 +607,7 @@ export function Workspace() {
}, [binaryStatus, prompt, cwd, busy, handle, start]);
const onStartFromSetup = useCallback(
async (args: any) => {
async (args: RunStartArgs) => {
if (busy) return;
setBusy("start");
setError(null);
@@ -621,29 +622,30 @@ export function Workspace() {
throw new Error(t("errors.cwdRequired"));
}
let targetLaneId = args.laneId;
if (!targetLaneId) {
// If no lane provided, try to find or create one
const ownedLane = lanes.find((l) => l.cwd === effectiveCwd);
if (ownedLane) {
targetLaneId = ownedLane.id;
setSelectedLaneId(ownedLane.id);
} else {
try {
const ensureResult = await api.lanes.ensure({ cwd: effectiveCwd });
targetLaneId = ensureResult.lane.id;
setSelectedLaneId(ensureResult.lane.id);
setLanes((prev) => {
const exists = prev.some((l) => l.id === ensureResult.lane.id);
return exists ? prev : [...prev, ensureResult.lane];
});
} catch (err) {
throw new Error(
t("errors.laneCreateFailed", {
message: err instanceof Error ? err.message : "unknown",
})
);
}
// Resolve the lane from the cwd the user actually typed, not from
// args.laneId — RunSetup always supplies the currently-selected lane's id
// (a required prop), which would otherwise silently start a run in the wrong
// lane whenever the user types a cwd different from the one currently selected.
const ownedLane = lanes.find((l) => l.cwd === effectiveCwd);
let targetLaneId: number;
if (ownedLane) {
targetLaneId = ownedLane.id;
if (ownedLane.id !== args.laneId) setSelectedLaneId(ownedLane.id);
} else {
try {
const ensureResult = await api.lanes.ensure({ cwd: effectiveCwd });
targetLaneId = ensureResult.lane.id;
setSelectedLaneId(ensureResult.lane.id);
setLanes((prev) => {
const exists = prev.some((l) => l.id === ensureResult.lane.id);
return exists ? prev : [...prev, ensureResult.lane];
});
} catch (err) {
throw new Error(
t("errors.laneCreateFailed", {
message: err instanceof Error ? err.message : "unknown",
})
);
}
}
@@ -4722,6 +4722,671 @@ exports[`screen snapshots > Claude Config 1`] = `
</div>
`;
exports[`screen snapshots > Dashboard 1`] = `
<div>
<div
class="flex flex-col gap-8 animate-fade-in min-h-[calc(100vh-4rem)]"
>
<div
class="flex flex-wrap items-center justify-between gap-3"
>
<div
class="flex items-center gap-3"
>
<div
class="w-9 h-9 rounded-xl bg-accent/15 flex items-center justify-center"
>
<svg
class="lucide lucide-layout-dashboard w-4.5 h-4.5 text-accent"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<rect
height="9"
rx="1"
width="7"
x="3"
y="3"
/>
<rect
height="5"
rx="1"
width="7"
x="14"
y="3"
/>
<rect
height="9"
rx="1"
width="7"
x="14"
y="12"
/>
<rect
height="5"
rx="1"
width="7"
x="3"
y="16"
/>
</svg>
</div>
<div>
<div
class="flex items-center gap-2"
>
<h1
class="text-lg font-semibold text-fg-primary"
>
Dashboard
</h1>
<span
class="flex items-center gap-1.5 text-[11px] text-status-success bg-status-success/10 border border-status-success/20 px-2 py-0.5 rounded-full"
>
<span
class="w-1.5 h-1.5 rounded-full bg-status-success animate-pulse-dot"
/>
Live
</span>
</div>
<p
class="text-xs text-fg-muted"
>
Real-time overview of Claude Code agent activity
</p>
</div>
</div>
<div
class="flex items-center gap-3"
>
<div
class="flex bg-surface-2 rounded-lg p-0.5 border border-border"
>
<button
class="px-2.5 py-1.5 rounded-md text-xs font-medium transition-all flex items-center gap-2 bg-accent/15 text-accent shadow-sm"
>
<svg
class="lucide lucide-activity w-3.5 h-3.5"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"
/>
</svg>
Monitor
</button>
<button
class="px-2.5 py-1.5 rounded-md text-xs font-medium transition-all flex items-center gap-2 text-fg-muted hover:text-fg-secondary"
>
<svg
class="lucide lucide-server w-3.5 h-3.5"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<rect
height="8"
rx="2"
ry="2"
width="20"
x="2"
y="2"
/>
<rect
height="8"
rx="2"
ry="2"
width="20"
x="2"
y="14"
/>
<line
x1="6"
x2="6.01"
y1="6"
y2="6"
/>
<line
x1="6"
x2="6.01"
y1="18"
y2="18"
/>
</svg>
Health
</button>
</div>
<button
class="btn-ghost flex-shrink-0"
>
<svg
class="lucide lucide-refresh-cw w-4 h-4"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8"
/>
<path
d="M21 3v5h-5"
/>
<path
d="M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16"
/>
<path
d="M8 16H3v5"
/>
</svg>
Refresh
</button>
</div>
</div>
<div
class="flex-1 flex flex-col gap-8 min-h-0"
>
<div
class="grid grid-cols-2 md:grid-cols-3 gap-4"
>
<div
class="card p-5"
>
<div
class="flex items-center justify-between gap-3 mb-3"
>
<span
class="text-xs font-medium text-fg-muted uppercase tracking-wider truncate"
>
Total Sessions
</span>
<svg
class="lucide lucide-folder-open w-5 h-5 flex-shrink-0 text-accent"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2"
/>
</svg>
</div>
<div
class="flex items-end gap-2 min-w-0"
>
<span
class="relative inline-block cursor-default"
>
<span
class="text-2xl font-semibold text-fg-primary truncate"
>
0
</span>
</span>
<span
class="text-xs text-fg-muted mb-1 flex-shrink-0"
>
0 active
</span>
</div>
</div>
<div
class="card p-5"
>
<div
class="flex items-center justify-between gap-3 mb-3"
>
<span
class="text-xs font-medium text-fg-muted uppercase tracking-wider truncate"
>
Active Agents
</span>
<svg
class="lucide lucide-bot w-5 h-5 flex-shrink-0 text-status-success"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 8V4H8"
/>
<rect
height="12"
rx="2"
width="16"
x="4"
y="8"
/>
<path
d="M2 14h2"
/>
<path
d="M20 14h2"
/>
<path
d="M15 13v2"
/>
<path
d="M9 13v2"
/>
</svg>
</div>
<div
class="flex items-end gap-2 min-w-0"
>
<span
class="text-2xl font-semibold text-fg-primary truncate"
>
0
</span>
</div>
</div>
<div
class="card p-5"
>
<div
class="flex items-center justify-between gap-3 mb-3"
>
<span
class="text-xs font-medium text-fg-muted uppercase tracking-wider truncate"
>
Active Subagents
</span>
<svg
class="lucide lucide-git-branch w-5 h-5 flex-shrink-0 text-violet-400"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="6"
x2="6"
y1="3"
y2="15"
/>
<circle
cx="18"
cy="6"
r="3"
/>
<circle
cx="6"
cy="18"
r="3"
/>
<path
d="M18 9a9 9 0 0 1-9 9"
/>
</svg>
</div>
<div
class="flex items-end gap-2 min-w-0"
>
<span
class="text-2xl font-semibold text-fg-primary truncate"
>
0
</span>
<span
class="text-xs text-fg-muted mb-1 flex-shrink-0"
>
0 in active sessions
</span>
</div>
</div>
<div
class="card p-5"
>
<div
class="flex items-center justify-between gap-3 mb-3"
>
<span
class="text-xs font-medium text-fg-muted uppercase tracking-wider truncate"
>
Events Today
</span>
<svg
class="lucide lucide-zap w-5 h-5 flex-shrink-0 text-yellow-400"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"
/>
</svg>
</div>
<div
class="flex items-end gap-2 min-w-0"
>
<span
class="relative inline-block cursor-default"
>
<span
class="text-2xl font-semibold text-fg-primary truncate"
>
0
</span>
</span>
</div>
</div>
<div
class="card p-5"
>
<div
class="flex items-center justify-between gap-3 mb-3"
>
<span
class="text-xs font-medium text-fg-muted uppercase tracking-wider truncate"
>
Total Events
</span>
<svg
class="lucide lucide-activity w-5 h-5 flex-shrink-0 text-violet-400"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"
/>
</svg>
</div>
<div
class="flex items-end gap-2 min-w-0"
>
<span
class="relative inline-block cursor-default"
>
<span
class="text-2xl font-semibold text-fg-primary truncate"
>
0
</span>
</span>
</div>
</div>
<div
class="card p-5"
>
<div
class="flex items-center justify-between gap-3 mb-3"
>
<span
class="text-xs font-medium text-fg-muted uppercase tracking-wider truncate"
>
Total Cost
</span>
<svg
class="lucide lucide-dollar-sign w-5 h-5 flex-shrink-0 text-status-success"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<line
x1="12"
x2="12"
y1="2"
y2="22"
/>
<path
d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"
/>
</svg>
</div>
<div
class="flex items-end gap-2 min-w-0"
>
<span
class="relative inline-block cursor-default"
>
<span
class="text-2xl font-semibold text-fg-primary truncate"
>
$0.00
</span>
</span>
</div>
</div>
</div>
<div
class="grid grid-cols-1 lg:grid-cols-[1fr_auto_1fr] gap-0 min-w-0 flex-1 min-h-0"
>
<div
class="min-w-0 overflow-y-auto pr-6"
>
<div
class="flex items-center justify-between mb-4"
>
<h3
class="text-sm font-medium text-fg-secondary"
>
Active Agents
</h3>
<button
class="btn-ghost text-xs"
>
View Board
<svg
class="lucide lucide-arrow-right w-3 h-3"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 12h14"
/>
<path
d="m12 5 7 7-7 7"
/>
</svg>
</button>
</div>
<div
class="flex flex-col items-center justify-center py-20 text-center"
>
<div
class="w-14 h-14 rounded-2xl bg-surface-4 flex items-center justify-center mb-5"
>
<svg
class="lucide lucide-bot w-6 h-6 text-fg-muted"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 8V4H8"
/>
<rect
height="12"
rx="2"
width="16"
x="4"
y="8"
/>
<path
d="M2 14h2"
/>
<path
d="M20 14h2"
/>
<path
d="M15 13v2"
/>
<path
d="M9 13v2"
/>
</svg>
</div>
<h3
class="text-base font-medium text-fg-secondary mb-2"
>
No active agents
</h3>
<p
class="text-sm text-fg-muted max-w-md mb-6"
>
Agents will appear here when a Claude Code session is running.
</p>
</div>
</div>
<div
class="hidden lg:block w-px bg-border self-stretch"
/>
<div
class="min-w-0 overflow-y-auto pl-6"
>
<div
class="flex items-center justify-between mb-4"
>
<h3
class="text-sm font-medium text-fg-secondary"
>
Recent Activity
</h3>
<button
class="btn-ghost text-xs"
>
View All
<svg
class="lucide lucide-arrow-right w-3 h-3"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M5 12h14"
/>
<path
d="m12 5 7 7-7 7"
/>
</svg>
</button>
</div>
<div
class="flex flex-col items-center justify-center py-20 text-center"
>
<div
class="w-14 h-14 rounded-2xl bg-surface-4 flex items-center justify-center mb-5"
>
<svg
class="lucide lucide-activity w-6 h-6 text-fg-muted"
fill="none"
height="24"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M22 12h-2.48a2 2 0 0 0-1.93 1.46l-2.35 8.36a.25.25 0 0 1-.48 0L9.24 2.18a.25.25 0 0 0-.48 0l-2.35 8.36A2 2 0 0 1 4.49 12H2"
/>
</svg>
</div>
<h3
class="text-base font-medium text-fg-secondary mb-2"
>
No activity yet
</h3>
<p
class="text-sm text-fg-muted max-w-md mb-6"
>
Events from Claude Code sessions will stream here in real-time.
</p>
</div>
</div>
</div>
</div>
</div>
</div>
`;
exports[`screen snapshots > Kanban board 1`] = `
<div>
<div
+28
View File
@@ -135,4 +135,32 @@ describe("pty-run", () => {
assert.equal(pty.laneIdFromRunId("ccam-lane-42"), 42);
assert.equal(pty.laneIdFromRunId("not-a-run-id"), null);
});
it("getRun returns the recorded prompt for a live run", () => {
let sessionExists = false;
tmux.__setExecImpl((args) => {
if (args[0] === "has-session") {
if (sessionExists) {
return ""; // session exists
}
// Session doesn't exist yet
const e = new Error("no such session");
e.status = 1;
throw e;
}
if (args[0] === "new-session") {
sessionExists = true; // Mark session as created
}
return "";
});
const handle = pty.spawnRun({
laneId: 99,
cwd: "/tmp/test",
initialPrompt: "the live prompt",
});
const retrieved = pty.getRun(handle.id);
assert.equal(retrieved.id, "ccam-lane-99");
assert.equal(retrieved.promptPreview, "the live prompt");
assert.equal(retrieved.status, "running");
});
});
+1
View File
@@ -152,6 +152,7 @@ function publicRun(id) {
resumeSessionId: row?.resume_session_id || null,
sessionId: row?.session_id || null,
startedAt: row?.started_at || null,
promptPreview: row?.prompt_preview || null,
};
}