From 4190e13a20c82ef56020c8d6ab8eca85cccde63d Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp <19+brynary@users.noreply.github.com> Date: Fri, 22 May 2026 12:20:29 -0400 Subject: [PATCH] Collapsible run stage sidebar (#352) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What Makes the run-detail stage sidebar (shown on the Overview and Stages tabs) collapsible with a slide animation. - A toggle button slides the panel between full width (`w-56`) and an icon-only rail (`w-12`), animating `width` over 300ms with the same easing as the Ask Fabro panel. - When collapsed, **stage status icons stay visible** β€” green check / red X / spinning teal for running β€” so run progress is still scannable at a glance. Workflow links (Graph Source, Run Logs, etc.) collapse to icons too so they remain reachable. - Labels and durations become `sr-only` with `title` tooltips for hover. - The open/closed choice persists to `localStorage` (`fabro:stage-sidebar-collapsed`), carrying across the Overview and Stages tabs and reloads. ## Layout - The collapse toggle is inline with the `STAGES` heading row (or `WORKFLOW` when a run has no stages yet), so it doesn't push the stage list down. - The stage sidebar's top padding on the Stages tab was reduced (`pt-6` β†’ `pt-3`) so the heading aligns with the adjacent content column and sits closer to the tab nav. ## Notes Self-contained in `StageSidebar` β€” `run-overview.tsx` and `run-stages.tsx` render it inside flex layouts that already track its width, so the slide works in both with no parent changes (aside from the padding tweak). Verified: `tsc` typecheck passes; `stage-sidebar` lib tests pass (10/10). πŸ€– Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) Co-authored-by: fabro-sh-0530[bot] <281434857+fabro-sh-0530[bot]@users.noreply.github.com> Co-authored-by: Fabro --- .../app/components/stage-sidebar.tsx | 272 +++++++++++------- apps/fabro-web/app/routes/run-detail.test.ts | 4 +- apps/fabro-web/app/routes/run-detail.tsx | 11 +- apps/fabro-web/app/routes/run-events.tsx | 8 +- apps/fabro-web/app/routes/run-sandbox.tsx | 13 +- apps/fabro-web/app/routes/run-stages.tsx | 4 +- .../app/routes/settings-resources.test.tsx | 3 +- apps/fabro-web/app/routes/start.tsx | 35 +-- .../administration/server-configuration.mdx | 17 +- docs/public/api-reference/fabro-api.yaml | 22 +- docs/public/changelog/2026-03-15.mdx | 8 - docs/public/execution/run-configuration.mdx | 2 +- docs/public/reference/user-configuration.mdx | 4 +- lib/crates/fabro-api/build.rs | 5 - lib/crates/fabro-api/src/lib.rs | 2 +- .../tests/server_settings_round_trip.rs | 13 +- lib/crates/fabro-cli/tests/it/cmd/config.rs | 5 +- lib/crates/fabro-config/src/builders.rs | 10 +- lib/crates/fabro-config/src/defaults.toml | 3 - lib/crates/fabro-config/src/layers/combine.rs | 2 - .../fabro-config/src/layers/features.rs | 14 - lib/crates/fabro-config/src/layers/mod.rs | 2 - .../fabro-config/src/layers/settings.rs | 12 - lib/crates/fabro-config/src/lib.rs | 11 +- lib/crates/fabro-config/src/parse.rs | 16 +- .../fabro-config/src/resolve/features.rs | 15 - lib/crates/fabro-config/src/resolve/mod.rs | 2 - lib/crates/fabro-config/src/tests/mod.rs | 1 - .../fabro-config/src/tests/resolve_cli.rs | 9 - .../src/tests/resolve_features.rs | 28 -- .../fabro-config/src/tests/resolve_server.rs | 11 - lib/crates/fabro-server/src/demo/mod.rs | 6 +- lib/crates/fabro-server/src/server.rs | 37 +-- .../fabro-server/src/server/handler/system.rs | 8 +- lib/crates/fabro-server/src/server/tests.rs | 43 --- lib/crates/fabro-server/tests/it/api/runs.rs | 6 - .../fabro-server/tests/it/api/settings.rs | 4 +- .../fabro-server/tests/it/api/system.rs | 6 +- lib/crates/fabro-types/src/dense.rs | 10 +- lib/crates/fabro-types/src/run_summary.rs | 1 - .../fabro-types/src/settings/features.rs | 12 - lib/crates/fabro-types/src/settings/mod.rs | 8 +- .../src/.openapi-generator/FILES | 2 - .../fabro-api-client/src/models/ask-fabro.ts | 1 - .../src/models/features-namespace.ts | 19 -- .../fabro-api-client/src/models/index.ts | 2 - .../src/models/server-settings.ts | 4 - .../src/models/system-features.ts | 25 -- .../src/models/system-info-response.ts | 4 - 49 files changed, 262 insertions(+), 500 deletions(-) delete mode 100644 lib/crates/fabro-config/src/layers/features.rs delete mode 100644 lib/crates/fabro-config/src/resolve/features.rs delete mode 100644 lib/crates/fabro-config/src/tests/resolve_features.rs delete mode 100644 lib/crates/fabro-types/src/settings/features.rs delete mode 100644 lib/packages/fabro-api-client/src/models/features-namespace.ts delete mode 100644 lib/packages/fabro-api-client/src/models/system-features.ts diff --git a/apps/fabro-web/app/components/stage-sidebar.tsx b/apps/fabro-web/app/components/stage-sidebar.tsx index a71ef183e..f9718077e 100644 --- a/apps/fabro-web/app/components/stage-sidebar.tsx +++ b/apps/fabro-web/app/components/stage-sidebar.tsx @@ -1,4 +1,4 @@ -import { type ComponentType } from "react"; +import { type ComponentType, type ReactNode, useCallback, useState } from "react"; import { Link } from "react-router"; import type { StageHandler, StageState } from "@qltysh/fabro-api-client"; import { @@ -12,6 +12,8 @@ import { import { Bars3BottomLeftIcon, BoltIcon, + ChevronDoubleLeftIcon, + ChevronDoubleRightIcon, DocumentTextIcon, MapIcon, PaperClipIcon, @@ -42,6 +44,90 @@ export const statusConfig: Record; + iconClass: string; + label: string; + /** Right-aligned text (e.g. stage duration); hidden when collapsed. */ + trailing?: string; + active: boolean; + collapsed: boolean; + /** Spin the icon to signal an in-flight stage. */ + spin?: boolean; +} + +/** A single sidebar link. The icon stays visible when collapsed; the label + * becomes screen-reader-only and a `title` tooltip stands in for sighted users. */ +function SidebarRow({ to, icon: Icon, iconClass, label, trailing, active, collapsed, spin }: SidebarRowProps) { + return ( +
  • + + + {label} + {trailing != null && !collapsed && ( + {trailing} + )} + +
  • + ); +} + +/** Section heading. When a `toggle` is given it sits inline with the heading + * (or stands alone, centered, when collapsed) rather than on its own row. */ +function SectionHeading({ + title, + collapsed, + toggle, +}: { + title: string; + collapsed: boolean; + toggle?: ReactNode; +}) { + if (collapsed) { + return ( + <> +

    {title}

    + {toggle &&
    {toggle}
    } + + ); + } + return ( +
    +

    {title}

    + {toggle} +
    + ); +} + interface StageSidebarProps { stages: Stage[]; runId: string; @@ -49,7 +135,30 @@ interface StageSidebarProps { activeLink?: "settings" | "source" | "logs" | "artifacts" | "events"; } +const WORKFLOW_LINKS: ReadonlyArray<{ + key: NonNullable; + path: string; + icon: ComponentType<{ className?: string }>; + label: string; +}> = [ + { key: "source", path: "source", icon: MapIcon, label: "Graph Source" }, + { key: "logs", path: "logs", icon: Bars3BottomLeftIcon, label: "Run Logs" }, + { key: "events", path: "events", icon: BoltIcon, label: "Run Events" }, + { key: "artifacts", path: "artifacts", icon: PaperClipIcon, label: "Artifacts" }, + { key: "settings", path: "settings", icon: DocumentTextIcon, label: "Run Settings" }, +]; + export function StageSidebar({ stages, runId, selectedStageId, activeLink }: StageSidebarProps) { + // Persisted so the choice carries across the Overview and Stages tabs. + const [collapsed, setCollapsed] = useState(loadStoredCollapsed); + const toggleCollapsed = useCallback(() => { + setCollapsed((prev) => { + const next = !prev; + persistCollapsed(next); + return next; + }); + }, []); + // Tick every second while any stage is running so the elapsed clock keeps up. const hasActive = stages.some((s) => ACTIVE_STAGE_STATES.has(s.status)); const now = useTickingNow(hasActive); @@ -62,108 +171,75 @@ export function StageSidebar({ stages, runId, selectedStageId, activeLink }: Sta return stage.duration; } - const linkBase = "flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors"; + const toggleButton = ( + + ); return ( - ); diff --git a/apps/fabro-web/app/routes/run-detail.test.ts b/apps/fabro-web/app/routes/run-detail.test.ts index 84a012e8a..a68b4a678 100644 --- a/apps/fabro-web/app/routes/run-detail.test.ts +++ b/apps/fabro-web/app/routes/run-detail.test.ts @@ -382,7 +382,7 @@ describe("RunDetail full-height child routes", () => { const outletWrappers = renderer.root.findAll( (node) => node.type === "div" && - hasClasses(node.props.className, ["mt-6", "min-h-0", "flex-1"]), + hasClasses(node.props.className, ["pt-3", "min-h-0", "flex-1"]), ); expect(outletWrappers).toHaveLength(1); }); @@ -541,7 +541,7 @@ describe("RunDetail full-height child routes", () => { (node) => node.type === "div" && hasClasses(node.props.className, [ - "mt-6", + "pt-3", "pb-[var(--fabro-interview-dock-clearance)]", ]), ); diff --git a/apps/fabro-web/app/routes/run-detail.tsx b/apps/fabro-web/app/routes/run-detail.tsx index 77eab29bc..ddc487430 100644 --- a/apps/fabro-web/app/routes/run-detail.tsx +++ b/apps/fabro-web/app/routes/run-detail.tsx @@ -700,8 +700,8 @@ export default function RunDetail({ params }: { params: { id: string } }) {
    @@ -744,10 +744,9 @@ const ASK_FABRO_UNAVAILABLE_TOOLTIPS: Record< AskFabroUnavailableReasonEnum, string > = { - [AskFabroUnavailableReasonEnum.FEATURE_DISABLED]: "Ask Fabro is disabled", - [AskFabroUnavailableReasonEnum.NO_SANDBOX]: "Run sandbox isn't ready", - [AskFabroUnavailableReasonEnum.SANDBOX_NOT_READY]:"Run sandbox isn't ready", - [AskFabroUnavailableReasonEnum.LLM_UNCONFIGURED]: "No LLM configured", + [AskFabroUnavailableReasonEnum.NO_SANDBOX]: "Run sandbox isn't ready", + [AskFabroUnavailableReasonEnum.SANDBOX_NOT_READY]: "Run sandbox isn't ready", + [AskFabroUnavailableReasonEnum.LLM_UNCONFIGURED]: "No LLM configured", }; function AskFabroTriggerButton({ diff --git a/apps/fabro-web/app/routes/run-events.tsx b/apps/fabro-web/app/routes/run-events.tsx index 2d12128a4..2f855a39c 100644 --- a/apps/fabro-web/app/routes/run-events.tsx +++ b/apps/fabro-web/app/routes/run-events.tsx @@ -28,8 +28,8 @@ export default function RunEvents() { ); return ( -
    -
    +
    +
    @@ -110,7 +110,7 @@ function EventsView({ if (error) { return ( -
    +
    +
    ); diff --git a/apps/fabro-web/app/routes/run-sandbox.tsx b/apps/fabro-web/app/routes/run-sandbox.tsx index b25c53704..729ac8f28 100644 --- a/apps/fabro-web/app/routes/run-sandbox.tsx +++ b/apps/fabro-web/app/routes/run-sandbox.tsx @@ -287,14 +287,13 @@ export default function RunSandbox({ params }: { params: { id: string } }) { }; // The outer flex spans from the tab bar's bottom border down to the - // steer bar β€” `-mt-6` cancels the outlet wrapper's top gap, and we - // intentionally omit `pb-[clearance]` here so the column divider can - // run the full height. Each column adds its own `pt-6` and dock - // clearance to its content instead. + // steer bar β€” `-mt-3` cancels the outlet wrapper's top padding so the + // column divider runs the full height, and we omit `pb-[clearance]` + // here. Each column adds its own `pt-3` and dock clearance instead. return ( -
    +