From 7fae2f48c046a021c756a6a4adcf48416530aad3 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Thu, 17 Sep 2026 11:37:01 +0700 Subject: [PATCH] refactor(run): drop the lane progress bar, iconify pane layout, and one-row the run setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four more spots that spent height saying what was already visible or saying too much of it. **LaneCard** drops the progress bar and percent entirely — the pipeline map right below the card already draws the same number as named, coloured nodes, and a bar beside it was a worse second telling of it. **SplitLayoutToggle** (Workspace) swaps "1 pane" / "2 pane" / "4 pane" text buttons for the conventional square / two-column / 2x2-grid icons (`Square`, `Columns2`, `Grid2x2`) most apps use for this. The words move to `aria-label` and `title` rather than disappearing. **RunSetup** merges the fresh/resume source picker onto the same row as the Working directory / Model / Permission mode / Thinking effort fields instead of a second row above them — settings left, New/Resume session right. The session picker for an active resume still gets its own row below, since it only appears in that one state. **LaneConsolePane**'s "Run Claude" header goes from a two-row title + wrapped blurb to one row (smaller icon and title), and the blurb itself is trimmed to one short sentence, hidden below `sm` where it would wrap again anyway. `docs/LANES.md` updated to match (split-view description, lane-card field list, and `agenthub stage`'s progress-bar wording). Verification: `tsc --noEmit` clean, `npx vitest run` 508/508 (2 stale progress-bar assertions in LaneCard.test.tsx replaced with one asserting the bar is gone), `npm run build` clean, screens.snapshot diff reviewed before regenerating (icon buttons + new blurb + reordered DOM only), file headers audit clean. --- client/src/components/lanes/LaneCard.tsx | 24 +- .../lanes/__tests__/LaneCard.test.tsx | 12 +- client/src/components/run/LaneConsolePane.tsx | 20 +- client/src/components/run/RunSetup.tsx | 163 +++---- client/src/i18n/locales/en/run.json | 2 +- client/src/i18n/locales/vi/run.json | 2 +- client/src/pages/Workspace.tsx | 48 +- .../screens.snapshot.test.tsx.snap | 428 ++++++++++-------- docs/LANES.md | 6 +- 9 files changed, 394 insertions(+), 311 deletions(-) diff --git a/client/src/components/lanes/LaneCard.tsx b/client/src/components/lanes/LaneCard.tsx index c98e21b2..81acac7c 100644 --- a/client/src/components/lanes/LaneCard.tsx +++ b/client/src/components/lanes/LaneCard.tsx @@ -1,12 +1,15 @@ /** - * @file One lane's card: progress bar, branch/CI/PR facts, the "needs you" + * @file One lane's card: branch/CI/PR facts, the "needs you" * banner sourced from Claude Code's Notification hook, and (for managed * lanes only) a ⋯ menu holding the rare `reset worktree` action behind * DestructiveLaneModal. A dead lane (its driving session went silent while * it should have been working) is called out loudly — that is the failure * this view exists to catch. Stage, kind, and the "auto: " chip are * NOT repeated here — this card only ever renders inside the Workspace - * lane-detail section, whose header above it already shows them. The repo + * lane-detail section, whose header above it already shows them. Neither is + * progress: the pipeline map directly below this card draws the same number + * as named, coloured nodes, so a bar and a percent beside it were a worse + * second telling of it. The repo * card also carries the lane's AI Router picker — the Add-lane modal is the * only other place one is chosen, and a lane outlives its creation. The repo * tooling dropdown (`LaneToolsMenu`, exported from this file) and the @@ -427,23 +430,6 @@ export default function LaneCard({ data-testid={`lane-card-${lane.id}`} className="flex flex-col rounded-lg border border-border bg-surface-4 p-2.5 transition-colors hover:border-border-light" > - {/* Progress line: how far through the pipeline. The stage's own name - and how long it's been running are already in the Workspace - header above; not repeated here. Omitted at 0% — an empty bar - is not information, just a blank row above a dead/idle lane. */} - {lane.progress > 0 && ( -
-
-
-
- {lane.progress}% -
- )} - {(lane.ci_status || lane.tool === "codex") && (
{lane.ci_status && ( diff --git a/client/src/components/lanes/__tests__/LaneCard.test.tsx b/client/src/components/lanes/__tests__/LaneCard.test.tsx index aee45937..68b13aa7 100644 --- a/client/src/components/lanes/__tests__/LaneCard.test.tsx +++ b/client/src/components/lanes/__tests__/LaneCard.test.tsx @@ -155,14 +155,12 @@ describe("LaneCard rebuilt layout", () => { expect(screen.getByTestId("lane-card-7")).toBeInTheDocument(); }); - it("shows the progress percentage", () => { + // The pipeline map below this card draws the same progress as named nodes, + // so the card no longer repeats it as a bar and a percent. + it("leaves progress to the pipeline map rather than drawing its own bar", () => { render(); - expect(screen.getByText("48%")).toBeInTheDocument(); - }); - - it("gives the progress bar a width matching the lane's progress", () => { - render(); - expect(screen.getByTestId("lane-progress-fill").getAttribute("style")).toContain("48%"); + expect(screen.queryByTestId("lane-progress-fill")).toBeNull(); + expect(screen.queryByText("48%")).toBeNull(); }); it("surfaces a needs-you message", () => { diff --git a/client/src/components/run/LaneConsolePane.tsx b/client/src/components/run/LaneConsolePane.tsx index 9252458e..bacb2a75 100644 --- a/client/src/components/run/LaneConsolePane.tsx +++ b/client/src/components/run/LaneConsolePane.tsx @@ -426,13 +426,17 @@ export function LaneConsolePane({ )} -
-
- + {/* Title, live badge and blurb on ONE line: the blurb is a standing + description of the panel, not a message, so it does not earn the + second row it used to wrap onto. It drops out entirely below `sm`, + where the width would wrap it again. */} +
+
+
-
-
-

+
+
+

{t("title", { tool: toolLabel })}

{wsConnected ? ( @@ -447,7 +451,9 @@ export function LaneConsolePane({ )}
-

{t("subtitle", { tool: toolLabel })}

+

+ {t("subtitle", { tool: toolLabel })} +

- {/* Fresh vs resume source picker */} -
-
+ {/* Run settings and the fresh-vs-resume source picker share one row: + settings left, source right. They used to be two rows with a divider + between them, which spent a band of height on four labelled selects + that are each one line tall. The resume picker below is the only part + that still gets a row of its own, and only while resume is chosen. */} +
+
+ + {isResume && props.resumeSession ? ( +
+ + {props.resumeSession.cwd} +
+ ) : ( +
+ +
+ )} +
+ + + + {props.tool === "codex" ? ( + <> + + + value={props.sandbox} + onChange={props.onSandboxChange} + options={[ + { value: "read-only", label: t("fields.sandboxReadOnly") }, + { value: "workspace-write", label: t("fields.sandboxWorkspaceWrite") }, + { value: "danger-full-access", label: t("fields.sandboxFullAccess") }, + ]} + /> + + + + value={props.approval} + onChange={props.onApprovalChange} + options={[ + { value: "on-request", label: t("fields.approvalOnRequest") }, + { value: "never", label: t("fields.approvalNever") }, + ]} + /> + + + ) : ( + + + value={props.permissionMode} + onChange={props.onPermissionModeChange} + options={[ + { value: "acceptEdits", label: t("fields.permissionAcceptEdits") }, + { value: "default", label: t("fields.permissionDefault") }, + { value: "plan", label: t("fields.permissionPlan") }, + { value: "bypassPermissions", label: t("fields.permissionBypass") }, + ]} + /> + + )} + + + value={props.effort} + onChange={props.onEffortChange} + options={RUN_EFFORT_CHOICES.map((c) => ({ + value: c.id, + label: c.label, + hint: c.hint, + }))} + /> + +
+
setResumePicked(true)} />
- {resumePicked && ( -
+
+ + {resumePicked && ( +
+
{props.tool === "claude" ? ( )}
- )} -
+
+ )} {/* Prompt */}
@@ -214,78 +291,6 @@ export function RunSetup(props: RunSetupProps) {
- {/* Advanced fields */} -
- - {isResume && props.resumeSession ? ( -
- - {props.resumeSession.cwd} -
- ) : ( -
- -
- )} -
- - - - {props.tool === "codex" ? ( - <> - - - value={props.sandbox} - onChange={props.onSandboxChange} - options={[ - { value: "read-only", label: t("fields.sandboxReadOnly") }, - { value: "workspace-write", label: t("fields.sandboxWorkspaceWrite") }, - { value: "danger-full-access", label: t("fields.sandboxFullAccess") }, - ]} - /> - - - - value={props.approval} - onChange={props.onApprovalChange} - options={[ - { value: "on-request", label: t("fields.approvalOnRequest") }, - { value: "never", label: t("fields.approvalNever") }, - ]} - /> - - - ) : ( - - - value={props.permissionMode} - onChange={props.onPermissionModeChange} - options={[ - { value: "acceptEdits", label: t("fields.permissionAcceptEdits") }, - { value: "default", label: t("fields.permissionDefault") }, - { value: "plan", label: t("fields.permissionPlan") }, - { value: "bypassPermissions", label: t("fields.permissionBypass") }, - ]} - /> - - )} - - - value={props.effort} - onChange={props.onEffortChange} - options={RUN_EFFORT_CHOICES.map((c) => ({ - value: c.id, - label: c.label, - hint: c.hint, - }))} - /> - -
- {props.tool === "codex" && props.sandbox === "danger-full-access" && (
diff --git a/client/src/i18n/locales/en/run.json b/client/src/i18n/locales/en/run.json index bbe6f699..88acc986 100644 --- a/client/src/i18n/locales/en/run.json +++ b/client/src/i18n/locales/en/run.json @@ -153,7 +153,7 @@ "permissionMode": "Mode", "session": "Session" }, - "subtitle": "Spin up a {{tool}} session right inside the dashboard. Live streaming output, multi-turn conversation, and the same hooks-driven analytics as your terminal sessions.", + "subtitle": "Streamed output, multi-turn, same hooks and analytics as your terminal.", "title": "Run {{tool}}", "tokens": { "cacheRead": "Cache hit", diff --git a/client/src/i18n/locales/vi/run.json b/client/src/i18n/locales/vi/run.json index 3be794d0..25f2677a 100644 --- a/client/src/i18n/locales/vi/run.json +++ b/client/src/i18n/locales/vi/run.json @@ -152,7 +152,7 @@ "permissionMode": "Mode", "session": "Session" }, - "subtitle": "Khởi chạy một phiên {{tool}} ngay trong dashboard. Stream output trực tiếp, hội thoại đa lượt, và chia sẻ cùng pipeline hooks/analytics với phiên ở terminal.", + "subtitle": "Stream trực tiếp, hội thoại đa lượt, cùng hooks và analytics như ở terminal.", "title": "Chạy {{tool}}", "tokens": { "cacheRead": "Cache hit", diff --git a/client/src/pages/Workspace.tsx b/client/src/pages/Workspace.tsx index ec667b06..d720680e 100644 --- a/client/src/pages/Workspace.tsx +++ b/client/src/pages/Workspace.tsx @@ -42,7 +42,7 @@ import { useCallback, useEffect, useState, useSyncExternalStore } from "react"; import { useTranslation } from "react-i18next"; -import { Plus } from "lucide-react"; +import { Plus, Square, Columns2, Grid2x2 } from "lucide-react"; import { api } from "../lib/api"; import type { CwdSuggestion, RunListResponse } from "../lib/api"; import type { @@ -72,6 +72,14 @@ import type { SplitViewState, SplitLayout } from "../lib/splitViewStorage"; // ── Page ────────────────────────────────────────────────────────────── +/** One pane, two side by side, four in a grid — the conventional icons for + * each, so the toggle reads at a glance without spending three words on it. */ +const LAYOUT_CHOICES = [ + { n: 1 as const, Icon: Square }, + { n: 2 as const, Icon: Columns2 }, + { n: 4 as const, Icon: Grid2x2 }, +]; + function SplitLayoutToggle({ splitView, setLayout, @@ -83,21 +91,29 @@ function SplitLayoutToggle({ return (
- {([1, 2, 4] as const).map((n) => ( - - ))} + {LAYOUT_CHOICES.map(({ n, Icon }) => { + // The count still reaches assistive tech and the tooltip — only the + // rendered word is gone, and the icon says the same thing in the + // shape every other app uses for it. + const label = tLanes("splitView.paneCount", { count: n }); + return ( + + ); + })}
); } diff --git a/client/src/pages/__tests__/__snapshots__/screens.snapshot.test.tsx.snap b/client/src/pages/__tests__/__snapshots__/screens.snapshot.test.tsx.snap index fa9c35b0..08db8759 100644 --- a/client/src/pages/__tests__/__snapshots__/screens.snapshot.test.tsx.snap +++ b/client/src/pages/__tests__/__snapshots__/screens.snapshot.test.tsx.snap @@ -5182,25 +5182,97 @@ exports[`screen snapshots > Run 1`] = ` class="flex items-center gap-1.5" >
Run 1`] = ` data-testid="console-body" >
Run 1`] = `

Run Claude

@@ -5251,9 +5323,9 @@ exports[`screen snapshots > Run 1`] = `

- Spin up a Claude session right inside the dashboard. Live streaming output, multi-turn conversation, and the same hooks-driven analytics as your terminal sessions. + Streamed output, multi-turn, same hooks and analytics as your terminal.

+
+
+

+
+ +
+ +
+
+
+ +
+ +
+
+
+
-
-
- -
-
-
- - - - -
-
-
-
-
- -
-
- -
-
-
-
- -
- -
-
-
- -
- -
-
-
diff --git a/docs/LANES.md b/docs/LANES.md index 0377fd79..0e2f6983 100644 --- a/docs/LANES.md +++ b/docs/LANES.md @@ -373,8 +373,8 @@ The dashboard web UI merges lanes and runs into a single **Workspace** page acce - **Header** — the page title and four counters (`lanes`, `running`, `needs you`, `dead`), plus Add lane. The `needs you` and `dead` counters appear only when they are non-zero, so a quiet header means nothing is waiting on a human. - **Detail panel** — the selected lane's declared stage, its inferred stage when detection leads, a full-width pipeline map, and a legend naming all five node states plus the dashed-amber inferred treatment. - **Terminal** — a real interactive terminal (xterm.js) displaying the tmux session's PTY output, with full support for interactive commands, editors, and pagers. A live run keeps its rendered history and scroll position when scrolling. -- **Split view** — a layout toggle (1 / 2 / 4 panes) renders that many independent terminal panes side by side (`grid-cols-2` for 2, a 2×2 grid for 4). Layout 1 is bound to the lane strip's selection, same as always; layouts 2 and 4 give each pane its own lane picker, independent of the strip. The chosen layout and each pane's lane persist to `localStorage` (`agenthub.workspace.splitView`) across reloads. -- **Lane grid** — one card per lane, 1 column, 2 at `md`, 3 at `xl`. Each card carries the lane id, liveness dot and status, title, declared stage with a progress bar and time-on-stage, the `auto:` chip when detection leads, the kind and CI tags, the working-copy facts from `GET /api/lanes/:id/git`, the needs-you banner, and the action row. +- **Split view** — a layout toggle (one pane / two side by side / a 2×2 grid, drawn as icons rather than the words, with the count kept as each button's `aria-label` and tooltip) renders that many independent terminal panes side by side (`grid-cols-2` for 2, a 2×2 grid for 4). Layout 1 is bound to the lane strip's selection, same as always; layouts 2 and 4 give each pane its own lane picker, independent of the strip. The chosen layout and each pane's lane persist to `localStorage` (`agenthub.workspace.splitView`) across reloads. +- **Lane grid** — one card per lane, 1 column, 2 at `md`, 3 at `xl`. Each card carries the lane id, liveness dot and status, title, declared stage and time-on-stage, the `auto:` chip when detection leads, the kind and CI tags, the working-copy facts from `GET /api/lanes/:id/git`, the needs-you banner, and the action row. Run history is per lane, queryable via `GET /api/run/history?laneId=`. @@ -617,7 +617,7 @@ agenthub stage implement --evidence "Schema created, migrations run" This command: - **Declares the stage** the work is now at - **Records evidence** (optional, but highly encouraged) — a short text description of what was completed -- **Moves the progress bar** forward in the pipeline +- **Advances the pipeline map** to that node - **Broadcasts to the UI** so connected dashboards see the update in real-time ### Full command signature