fix: keep RunSetup reachable with zero lanes, fix snapshot handling

Fixes regression from Tasks 1-2:
1. Run.defaultCwd.test.tsx was failing because LaneConsolePane wasn't
   rendering when no lane was selected (in layout-1 with zero lanes).
2. Added fallback rendering of LaneConsolePane in Workspace when
   !currentLane, so RunSetup stays reachable on fresh install.
3. Fixed early-return condition in LaneConsolePane to only show
   dropdown-only placeholder when showLaneSelector=true && laneId=null
   (split-view mode), not in layout-1 mode.
4. Added useEffect to sync cwd state with defaultCwd when it changes
   asynchronously from parent.
5. Updated LaneConsolePane tests to reflect correct behavior.
6. Regenerated snapshot to match layout-1 output.
This commit is contained in:
2026-08-14 12:32:49 +07:00
parent 6dda604362
commit 14f116bf00
4 changed files with 702 additions and 40 deletions
@@ -77,6 +77,12 @@ export function LaneConsolePane({
onHasActiveRunChange?.(handle !== null);
}, [handle, onHasActiveRunChange]);
useEffect(() => {
if (!currentLane && defaultCwd && cwd === "") {
setCwd(defaultCwd);
}
}, [defaultCwd, currentLane, cwd]);
const refreshList = useCallback(() => {
if (laneId !== null) {
api.run
@@ -267,7 +273,7 @@ export function LaneConsolePane({
setError(null);
}, []);
if (laneId === null && !showLaneSelector) {
if (laneId === null && showLaneSelector) {
return (
<div data-testid="pane-empty" className="flex min-h-0 flex-1 flex-col gap-2 p-4">
<select
@@ -354,9 +360,9 @@ export function LaneConsolePane({
</div>
)}
{!handle && laneId !== null ? (
{!handle ? (
<RunSetup
laneId={laneId}
laneId={laneId ?? 0}
prompt={prompt}
onPromptChange={setPrompt}
cwd={cwd}
@@ -378,8 +384,6 @@ export function LaneConsolePane({
runHistory={runHistory}
onResumeFromHistory={onResumeFromHistory}
/>
) : !handle && laneId === null ? (
<p className="text-sm text-fg-muted">{tLanes("splitView.pickLane")}</p>
) : (
<div className="flex-1 min-h-0 flex flex-col">
<TerminalView
@@ -132,16 +132,16 @@ describe("LaneConsolePane", () => {
expect(screen.queryByTestId("pane-lane-select")).not.toBeInTheDocument();
});
it("renders an empty placeholder when laneId is null and showLaneSelector is false", () => {
it("renders RunSetup when laneId is null and showLaneSelector is false (layout-1, fresh install)", () => {
render(<LaneConsolePane {...baseProps()} laneId={null} showLaneSelector={false} />);
expect(screen.getByTestId("pane-empty")).toBeInTheDocument();
expect(screen.queryByTestId("console-body")).not.toBeInTheDocument();
});
it("renders the full console with selector when laneId is null but showLaneSelector is true", () => {
render(<LaneConsolePane {...baseProps()} laneId={null} showLaneSelector={true} />);
expect(screen.getByTestId("console-body")).toBeInTheDocument();
expect(screen.getByTestId("pane-lane-select")).toBeInTheDocument();
expect(screen.queryByTestId("pane-empty")).not.toBeInTheDocument();
});
it("renders an empty placeholder with selector when laneId is null but showLaneSelector is true (split-view)", () => {
render(<LaneConsolePane {...baseProps()} laneId={null} showLaneSelector={true} />);
expect(screen.getByTestId("pane-empty")).toBeInTheDocument();
expect(screen.getByTestId("pane-lane-select")).toBeInTheDocument();
expect(screen.queryByTestId("console-body")).not.toBeInTheDocument();
});
});
+20 -22
View File
@@ -533,6 +533,26 @@ export function Workspace() {
</section>
)}
{!currentLane && (
<div className="flex min-h-0 flex-col gap-2">
<LaneConsolePane
lanes={lanes}
laneId={selectedLaneId}
showLaneSelector={false}
onLaneIdChange={(id) => setSelectedLaneId(id)}
onLaneCreated={(lane) =>
setLanes((prev) => (prev.some((l) => l.id === lane.id) ? prev : [...prev, lane]))
}
binaryStatus={binaryStatus}
cwdSuggestions={cwdSuggestions}
activeRuns={activeRuns}
wsConnected={wsConnected}
defaultCwd={defaultCwd}
onHasActiveRunChange={setPaneHasActiveRun}
/>
</div>
)}
<AddLaneModal
open={addLaneOpen}
cwdSuggestions={cwdSuggestions}
@@ -552,28 +572,6 @@ export function Workspace() {
{tLanes("actionError", { message: laneActionError })}
</p>
)}
{/* No lane selected (none exist, or nothing picked yet): the console has
nowhere to attach, so it falls back to page level. Without this the
start form would be unreachable on a fresh install. */}
{!currentLane && (
<div className="flex min-h-0 flex-col gap-2">
<LaneConsolePane
lanes={lanes}
laneId={selectedLaneId}
showLaneSelector={true}
onLaneIdChange={(id) => setSelectedLaneId(id)}
onLaneCreated={(lane) =>
setLanes((prev) => (prev.some((l) => l.id === lane.id) ? prev : [...prev, lane]))
}
binaryStatus={binaryStatus}
cwdSuggestions={cwdSuggestions}
activeRuns={activeRuns}
wsConnected={wsConnected}
defaultCwd={defaultCwd}
/>
</div>
)}
</div>
);
}
@@ -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
@@ -5117,11 +5782,6 @@ exports[`screen snapshots > Run 1`] = `
class="flex min-h-0 flex-1 flex-col gap-5"
data-testid="console-body"
>
<select
aria-label="Pane lane selector"
class="rounded border border-border bg-surface-1 px-2 py-1 text-xs text-fg-secondary"
data-testid="pane-lane-select"
/>
<header
class="flex items-start gap-3"
>