27923db4e0
The Workspace lane panel spent four rows saying what fits in two. Strip tile: the stage badges move onto the id row and the progress bar and percent go away — the detail panel's pipeline map right below shows the same progress with the stage names attached. Lane detail: the pipeline picker, stage badges, feature picker, router select and the remove button merge into the single header row instead of a second row under it, and the pipeline's name is no longer interpolated into `laneHeader` — the picker beside it already says it. The git block collapses from four lines to one and drops the cwd, which the header above it already spells out. The router select is relabelled Model, moves out of LaneCard into that header row (`LaneRouterRow` is now exported), and reports its apply outcome as a ✓/⚠ marker with the message in a tooltip rather than a wrapped sentence that pushes every row below it down. Drops the tracker / dev QC / CI wait badge row and everything feeding it (`useLaneIntegrations`, `api.lanes.integration`, the `integrations.*` strings): two of the three toggles have no consumer at all, and a badge reporting a toggle nothing acts on costs a row of height to say nothing. `agenthub lanes integration` and the API route are untouched.
106 lines
3.9 KiB
TypeScript
106 lines
3.9 KiB
TypeScript
/**
|
|
* @file The compact lane tile used in the Workspace's horizontal lane strip.
|
|
* It carries only what you need to pick a lane — which lane, is it alive,
|
|
* what stage — because the full card, its controls and its working-copy facts
|
|
* live in the detail panel below it. Stage badges ride on the id row rather
|
|
* than a line of their own, and progress (bar and percent) is deliberately
|
|
* absent: the detail panel's pipeline map below shows the same thing with the
|
|
* stage names attached. Keeping the tile small and fixed-width is what lets
|
|
* many lanes stay scannable in one scrolling row.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { useTranslation } from "react-i18next";
|
|
import type { Lane } from "../../lib/types";
|
|
|
|
const LIVENESS_DOT: Record<Lane["liveness"], string> = {
|
|
active: "bg-status-success",
|
|
idle: "bg-surface-4",
|
|
dead: "bg-status-danger",
|
|
};
|
|
|
|
/**
|
|
* Whether the inferred stage sits ahead of the declared one in node order.
|
|
*
|
|
* The declaration is located by the node the server resolved it onto (`state
|
|
* === "current"`), not by `indexOf(lane.stage)`: a stage declared under an
|
|
* ALIAS (`tests`, `booting`, `ship` — most of what the ship-feature skill
|
|
* declares) is not a node id, so indexOf answers -1 and a detection sitting
|
|
* well BEHIND the declaration would still be badged as the live one.
|
|
*
|
|
* `>=`, not `>`: an undeclared lane is rendered AT its detected node by the
|
|
* server (see `inferredDisplayStage`), so the detection and the "current" node
|
|
* are the same one — and that lane is precisely the one whose stage the badge
|
|
* is the only readout for. A declared lane can never tie, because
|
|
* recordDetection refuses any detection that isn't strictly ahead.
|
|
*/
|
|
function detectionLeads(lane: Lane): boolean {
|
|
if (!lane.detected_stage) return false;
|
|
const nodes = lane.pipeline_nodes;
|
|
return (
|
|
nodes.findIndex((n) => n.id === lane.detected_stage) >=
|
|
nodes.findIndex((n) => n.state === "current")
|
|
);
|
|
}
|
|
|
|
export default function LaneStripCard({
|
|
lane,
|
|
selected,
|
|
onSelect,
|
|
}: {
|
|
lane: Lane;
|
|
selected: boolean;
|
|
onSelect: () => void;
|
|
}) {
|
|
const { t } = useTranslation(["lanes"]);
|
|
|
|
return (
|
|
<button
|
|
type="button"
|
|
data-testid={`lane-tile-${lane.id}`}
|
|
data-selected={selected ? "true" : undefined}
|
|
aria-pressed={selected}
|
|
onClick={onSelect}
|
|
title={lane.cwd}
|
|
className={`w-56 shrink-0 rounded-lg border p-3 text-left shadow-sm transition-colors ${
|
|
selected
|
|
? "border-accent bg-accent/10"
|
|
: "border-border bg-surface-2 hover:border-border-light hover:bg-surface-3"
|
|
}`}
|
|
>
|
|
<div className="mb-1.5 flex items-center gap-1.5 text-[11px]">
|
|
<span className={`h-2 w-2 shrink-0 rounded-full ${LIVENESS_DOT[lane.liveness]}`} />
|
|
<span className="shrink-0 text-[10px] font-semibold uppercase tracking-widest text-fg-muted">
|
|
{t("cardId", { id: lane.id })}
|
|
</span>
|
|
<span className="truncate rounded bg-surface-4 px-1.5 py-0.5 text-fg-secondary">
|
|
{lane.stage}
|
|
</span>
|
|
{lane.tool === "codex" && (
|
|
<span className="shrink-0 rounded border border-border bg-surface-2 px-1.5 py-0.5 text-[10px] text-fg-muted">
|
|
Codex
|
|
</span>
|
|
)}
|
|
{detectionLeads(lane) && (
|
|
<span
|
|
data-testid={`lane-tile-auto-${lane.id}`}
|
|
title={lane.detected_signal || undefined}
|
|
className="shrink-0 rounded border border-dashed border-status-warning/50 px-1 text-status-warning"
|
|
>
|
|
{lane.detected_stage}
|
|
</span>
|
|
)}
|
|
{lane.needs_action && (
|
|
<span className="ml-auto shrink-0 text-status-warning" title={lane.needs_action}>
|
|
⚠
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<div className="truncate text-[13px] font-medium text-fg-primary">
|
|
{lane.title || lane.cwd}
|
|
</div>
|
|
</button>
|
|
);
|
|
}
|