From 0e38d42cf9729058e7613f637d3d8a3cf0de81cf Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Thu, 17 Sep 2026 13:20:14 +0700 Subject: [PATCH] refactor(workspace): drop the page header, move Add lane and Tools onto the lanes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The title and the four counters said a second time what the lane strip already shows lane by lane (liveness dot, ⚠ marker), so they only cost a row of height on a screen whose point is the terminal. `GET /api/lanes` still returns `counts` for other callers. Add lane becomes the strip's FIRST tile — creating a lane is the same kind of act as picking one — and the per-lane Tools menu (agents install / mcp sync / sync-base check) moves onto the lane-detail header row beside the pipeline picker and the Model select, next to the lane it acts on. --- client/src/pages/Workspace.tsx | 82 +++++-------------- client/src/pages/__tests__/Workspace.test.tsx | 30 ++----- .../screens.snapshot.test.tsx.snap | 45 ++-------- 3 files changed, 35 insertions(+), 122 deletions(-) diff --git a/client/src/pages/Workspace.tsx b/client/src/pages/Workspace.tsx index d720680e..f8000406 100644 --- a/client/src/pages/Workspace.tsx +++ b/client/src/pages/Workspace.tsx @@ -2,8 +2,8 @@ * @file Workspace.tsx * @description Merged workspace page combining lanes (agent work units) and runs * (CLI spawns: Claude Code or Codex, determined per lane). Displays a horizontal - * lane strip at the top with counters, the selected lane's pipeline map, and run - * configuration/console/history below. Runs are tied to lanes: starting a run + * lane strip at the top (its first tile creates a lane), the selected lane's + * pipeline map, and run configuration/console/history below. Runs are tied to lanes: starting a run * posts to POST /api/lanes/:id/start with tool-appropriate fields. When a cwd * isn't owned by any lane, calls POST /api/lanes/ensure first. Polls GET * /api/sessions?status=active alongside the run list so Claude Code sessions @@ -45,15 +45,7 @@ import { useTranslation } from "react-i18next"; import { Plus, Square, Columns2, Grid2x2 } from "lucide-react"; import { api } from "../lib/api"; import type { CwdSuggestion, RunListResponse } from "../lib/api"; -import type { - Lane, - LaneFeature, - LaneCounts, - ProofFeature, - Session, - WSMessage, - LaneTool, -} from "../lib/types"; +import type { Lane, LaneFeature, ProofFeature, Session, WSMessage, LaneTool } from "../lib/types"; import { eventBus } from "../lib/eventBus"; import { keepIfSame } from "../lib/keepIfSame"; import { LaneConsolePane } from "../components/run/LaneConsolePane"; @@ -203,7 +195,6 @@ export function Workspace() { // Lane state const [lanes, setLanes] = useState([]); - const [counts, setCounts] = useState({ total: 0, running: 0, needs_you: 0, dead: 0 }); const [selectedLaneId, setSelectedLaneId] = useState(null); const [laneActionError, setLaneActionError] = useState(null); const [addLaneOpen, setAddLaneOpen] = useState(false); @@ -263,7 +254,6 @@ export function Workspace() { try { const r = await api.lanes.list(); setLanes(keepIfSame(r.lanes)); - setCounts(keepIfSame(r.counts)); setSelectedLaneId((cur) => cur !== null && r.lanes.some((l) => l.id === cur) ? cur : (r.lanes[0]?.id ?? null) ); @@ -515,60 +505,25 @@ export function Workspace() { : "space-y-5" } > - {/* Page header: what this screen is, and the four numbers that say - whether anything needs a human right now. */} -
-

- {tLanes("title")} -

-
- - {counts.total} {tLanes("countTotal")} - - - {counts.running} {tLanes("countRunning")} - - {counts.needs_you > 0 && ( - - {counts.needs_you} {tLanes("countNeedsYou")} - - )} - {counts.dead > 0 && ( - - {counts.dead} {tLanes("countDead")} - - )} -
-
- {currentLane && } - -
-
- {/* Lane strip + detail stack vertically: the strip is a horizontally scrolling row (holds any number of lanes), the detail/console area below it gets the full width and never competes for it. */}
+ {/* "Add lane" is a tile in the strip rather than a header button: + creating a lane is the same kind of act as picking one, so it + reads as the empty slot at the head of the row. Dashed border and + no liveness dot keep it from being mistaken for a lane. */} + {lanes.map((l) => ( )}
+ {/* Per-lane actions (agents install, mcp sync, sync-base check) + belong beside the lane they act on, not in a page header. */} + -
-
-
-