From 06817b79013c368f1519aefbb69ac0be205592a1 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Fri, 14 Aug 2026 12:55:37 +0700 Subject: [PATCH] feat(workspace): add 1/2/4-pane split terminal view toggle --- client/src/i18n/locales/en/lanes.json | 1 + client/src/i18n/locales/vi/lanes.json | 1 + client/src/pages/Workspace.tsx | 170 ++++++++++++++++++++++---- 3 files changed, 151 insertions(+), 21 deletions(-) diff --git a/client/src/i18n/locales/en/lanes.json b/client/src/i18n/locales/en/lanes.json index 2204591..91bbe91 100644 --- a/client/src/i18n/locales/en/lanes.json +++ b/client/src/i18n/locales/en/lanes.json @@ -122,6 +122,7 @@ "proof.ticketReport": "Task report", "splitView.emptyPane": "No lane selected for this pane.", "splitView.paneLaneLabel": "Pane lane selector", + "splitView.paneCount": "{{count}} pane", "splitView.pickLane": "Pick a lane", "statusDead": "DEAD", "title": "Lanes", diff --git a/client/src/i18n/locales/vi/lanes.json b/client/src/i18n/locales/vi/lanes.json index 230acdf..dd40056 100644 --- a/client/src/i18n/locales/vi/lanes.json +++ b/client/src/i18n/locales/vi/lanes.json @@ -122,6 +122,7 @@ "proof.ticketReport": "Báo cáo nhiệm vụ", "splitView.emptyPane": "Chưa chọn lane cho ô này.", "splitView.paneLaneLabel": "Bộ chọn lane cho ô", + "splitView.paneCount": "{{count}} ô", "splitView.pickLane": "Chọn lane", "statusDead": "ĐÃ CHẾT", "title": "Làn đường", diff --git a/client/src/pages/Workspace.tsx b/client/src/pages/Workspace.tsx index 8af732d..85fca3e 100644 --- a/client/src/pages/Workspace.tsx +++ b/client/src/pages/Workspace.tsx @@ -49,6 +49,8 @@ import PipelineMap from "../components/lanes/PipelineMap"; import LaneCard from "../components/lanes/LaneCard"; import LaneStripCard from "../components/lanes/LaneStripCard"; import { AddLaneModal } from "../components/lanes/AddLaneModal"; +import { readSplitViewState, writeSplitViewState } from "../lib/splitViewStorage"; +import type { SplitViewState, SplitLayout } from "../lib/splitViewStorage"; // ── Page ────────────────────────────────────────────────────────────── @@ -80,6 +82,28 @@ export function Workspace() { const [defaultCwd, setDefaultCwd] = useState(""); const [paneHasActiveRun, setPaneHasActiveRun] = useState(false); + // Split view state + const [splitView, setSplitView] = useState(() => readSplitViewState()); + + const setLayout = useCallback((layout: SplitLayout) => { + setSplitView((prev) => { + const paneLaneIds = Array.from({ length: layout }, (_, i) => prev.paneLaneIds[i] ?? null); + const next = { layout, paneLaneIds }; + writeSplitViewState(next); + return next; + }); + }, []); + + const setPaneLaneId = useCallback((index: number, id: number) => { + setSplitView((prev) => { + const paneLaneIds = [...prev.paneLaneIds]; + paneLaneIds[index] = id; + const next = { ...prev, paneLaneIds }; + writeSplitViewState(next); + return next; + }); + }, []); + // Pre-flight: probe binary + active runs + cwd suggestions + lanes on mount const refreshLanes = useCallback(async () => { try { @@ -167,6 +191,20 @@ export function Workspace() { [refreshLanes] ); + // Clean up stale pane lane IDs when lanes load + useEffect(() => { + if (!lanes.length) return; + setSplitView((prev) => { + const paneLaneIds = prev.paneLaneIds.map((id) => + id !== null && lanes.some((l) => l.id === id) ? id : null + ); + if (paneLaneIds.every((id, i) => id === prev.paneLaneIds[i])) return prev; + const next = { ...prev, paneLaneIds }; + writeSplitViewState(next); + return next; + }); + }, [lanes]); + const refreshList = useCallback(() => { api.run .list() @@ -514,6 +552,90 @@ export function Workspace() { )}
+
+ {([1, 2, 4] as const).map((n) => ( + + ))} +
+ {splitView.layout === 1 ? ( + 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} + /> + ) : ( +
+ {splitView.paneLaneIds.map((id, i) => ( + 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} + /> + ))} +
+ )} +
+ + )} + + {!currentLane && ( +
+
+ {([1, 2, 4] as const).map((n) => ( + + ))} +
+ {splitView.layout === 1 ? ( -
- - )} - - {!currentLane && ( -
- 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} - /> + ) : ( +
+ {splitView.paneLaneIds.map((id, i) => ( + 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} + /> + ))} +
+ )}
)}