refactor: dedupe split-view render, strengthen persistence test

- Extract ConsoleArea helper to eliminate ~60 lines of duplicated layout
  toggle + grid rendering shared between currentLane and !currentLane branches
- Fix persistence test to validate the component's actual write path instead
  of manually re-seeding localStorage (proves writeSplitViewState is called)
- Remove orphaned grid/pane code left by incomplete merge
This commit is contained in:
2026-08-14 13:36:02 +07:00
parent d542fbbf4b
commit 6f22aed47c
2 changed files with 123 additions and 124 deletions
+123 -118
View File
@@ -54,6 +54,97 @@ import type { SplitViewState, SplitLayout } from "../lib/splitViewStorage";
// ── Page ────────────────────────────────────────────────────────────── // ── Page ──────────────────────────────────────────────────────────────
function ConsoleArea({
lanes,
selectedLaneId,
splitView,
setLayout,
setPaneLaneId,
binaryStatus,
cwdSuggestions,
activeRuns,
wsConnected,
defaultCwd,
onHasActiveRunChange,
onLaneCreated,
onLaneIdChange,
}: {
lanes: Lane[];
selectedLaneId: number | null;
splitView: SplitViewState;
setLayout: (layout: SplitLayout) => void;
setPaneLaneId: (index: number, id: number) => void;
binaryStatus: { found: boolean; path: string | null } | null;
cwdSuggestions: CwdSuggestion[];
activeRuns: RunListResponse | null;
wsConnected: boolean;
defaultCwd: string;
onHasActiveRunChange: (val: boolean) => void;
onLaneCreated: (lane: Lane) => void;
onLaneIdChange: (id: number | null) => void;
}) {
const { t: tLanes } = useTranslation("lanes");
return (
<>
<div className="flex items-center gap-1.5">
{([1, 2, 4] as const).map((n) => (
<button
key={n}
type="button"
aria-pressed={splitView.layout === n}
onClick={() => setLayout(n)}
className={`rounded border px-2 py-1 text-xs ${
splitView.layout === n
? "border-accent bg-accent/15 text-accent"
: "border-border text-fg-secondary hover:border-border-light"
}`}
>
{tLanes("splitView.paneCount", { count: n })}
</button>
))}
</div>
{splitView.layout === 1 ? (
<LaneConsolePane
lanes={lanes}
laneId={selectedLaneId}
showLaneSelector={false}
onLaneIdChange={onLaneIdChange}
onLaneCreated={onLaneCreated}
binaryStatus={binaryStatus}
cwdSuggestions={cwdSuggestions}
activeRuns={activeRuns}
wsConnected={wsConnected}
defaultCwd={defaultCwd}
onHasActiveRunChange={onHasActiveRunChange}
/>
) : (
<div
className={`grid flex-1 min-h-0 gap-3 ${
splitView.layout === 2 ? "grid-cols-2" : "grid-cols-2 grid-rows-2"
}`}
>
{splitView.paneLaneIds.map((id, i) => (
<LaneConsolePane
key={i}
lanes={lanes}
laneId={id}
showLaneSelector
onLaneIdChange={(newId) => setPaneLaneId(i, newId)}
onLaneCreated={onLaneCreated}
binaryStatus={binaryStatus}
cwdSuggestions={cwdSuggestions}
activeRuns={activeRuns}
wsConnected={wsConnected}
defaultCwd={defaultCwd}
/>
))}
</div>
)}
</>
);
}
export function Workspace() { export function Workspace() {
const { t: tLanes } = useTranslation("lanes"); const { t: tLanes } = useTranslation("lanes");
const wsConnected = useSyncExternalStore(eventBus.onConnection, () => eventBus.connected); const wsConnected = useSyncExternalStore(eventBus.onConnection, () => eventBus.connected);
@@ -552,132 +643,46 @@ export function Workspace() {
</div> </div>
)} )}
<div className="flex min-h-0 flex-col gap-2 border-t border-border pt-3"> <div className="flex min-h-0 flex-col gap-2 border-t border-border pt-3">
<div className="flex items-center gap-1.5"> <ConsoleArea
{([1, 2, 4] as const).map((n) => (
<button
key={n}
type="button"
aria-pressed={splitView.layout === n}
onClick={() => setLayout(n)}
className={`rounded border px-2 py-1 text-xs ${
splitView.layout === n
? "border-accent bg-accent/15 text-accent"
: "border-border text-fg-secondary hover:border-border-light"
}`}
>
{tLanes("splitView.paneCount", { count: n })}
</button>
))}
</div>
{splitView.layout === 1 ? (
<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
className={`grid flex-1 min-h-0 gap-3 ${
splitView.layout === 2 ? "grid-cols-2" : "grid-cols-2 grid-rows-2"
}`}
>
{splitView.paneLaneIds.map((id, i) => (
<LaneConsolePane
key={i}
lanes={lanes}
laneId={id}
showLaneSelector
onLaneIdChange={(newId) => setPaneLaneId(i, newId)}
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>
</section>
)}
{!currentLane && (
<div className="flex min-h-0 flex-col gap-2">
<div className="flex items-center gap-1.5">
{([1, 2, 4] as const).map((n) => (
<button
key={n}
type="button"
aria-pressed={splitView.layout === n}
onClick={() => setLayout(n)}
className={`rounded border px-2 py-1 text-xs ${
splitView.layout === n
? "border-accent bg-accent/15 text-accent"
: "border-border text-fg-secondary hover:border-border-light"
}`}
>
{tLanes("splitView.paneCount", { count: n })}
</button>
))}
</div>
{splitView.layout === 1 ? (
<LaneConsolePane
lanes={lanes} lanes={lanes}
laneId={selectedLaneId} selectedLaneId={selectedLaneId}
showLaneSelector={false} splitView={splitView}
onLaneIdChange={(id) => setSelectedLaneId(id)} setLayout={setLayout}
onLaneCreated={(lane) => setPaneLaneId={setPaneLaneId}
setLanes((prev) => (prev.some((l) => l.id === lane.id) ? prev : [...prev, lane]))
}
binaryStatus={binaryStatus} binaryStatus={binaryStatus}
cwdSuggestions={cwdSuggestions} cwdSuggestions={cwdSuggestions}
activeRuns={activeRuns} activeRuns={activeRuns}
wsConnected={wsConnected} wsConnected={wsConnected}
defaultCwd={defaultCwd} defaultCwd={defaultCwd}
onHasActiveRunChange={setPaneHasActiveRun} onHasActiveRunChange={setPaneHasActiveRun}
onLaneCreated={(lane) =>
setLanes((prev) => (prev.some((l) => l.id === lane.id) ? prev : [...prev, lane]))
}
onLaneIdChange={setSelectedLaneId}
/> />
) : ( </div>
<div </section>
className={`grid flex-1 min-h-0 gap-3 ${ )}
splitView.layout === 2 ? "grid-cols-2" : "grid-cols-2 grid-rows-2"
}`} {!currentLane && (
> <div className="flex min-h-0 flex-col gap-2">
{splitView.paneLaneIds.map((id, i) => ( <ConsoleArea
<LaneConsolePane lanes={lanes}
key={i} selectedLaneId={selectedLaneId}
lanes={lanes} splitView={splitView}
laneId={id} setLayout={setLayout}
showLaneSelector setPaneLaneId={setPaneLaneId}
onLaneIdChange={(newId) => setPaneLaneId(i, newId)} binaryStatus={binaryStatus}
onLaneCreated={(lane) => cwdSuggestions={cwdSuggestions}
setLanes((prev) => activeRuns={activeRuns}
prev.some((l) => l.id === lane.id) ? prev : [...prev, lane] wsConnected={wsConnected}
) defaultCwd={defaultCwd}
} onHasActiveRunChange={setPaneHasActiveRun}
binaryStatus={binaryStatus} onLaneCreated={(lane) =>
cwdSuggestions={cwdSuggestions} setLanes((prev) => (prev.some((l) => l.id === lane.id) ? prev : [...prev, lane]))
activeRuns={activeRuns} }
wsConnected={wsConnected} onLaneIdChange={setSelectedLaneId}
defaultCwd={defaultCwd} />
/>
))}
</div>
)}
</div> </div>
)} )}
@@ -671,12 +671,6 @@ describe("split terminal view", () => {
await settle(); await settle();
unmount(); unmount();
localStorage.clear();
localStorage.setItem(
"ccam.workspace.splitView",
JSON.stringify({ layout: 2, paneLaneIds: [null, lanesToReturn[1]!.id] })
);
await renderWorkspace(); await renderWorkspace();
const persistedSelects = screen.getAllByTestId("pane-lane-select"); const persistedSelects = screen.getAllByTestId("pane-lane-select");
expect(persistedSelects).toHaveLength(2); expect(persistedSelects).toHaveLength(2);