feat(lanes): add pipeline-template picker to the Workspace lane header
Lets a lane's pipeline template be switched live from the dashboard (the same PATCH /api/lanes/:id the CLI's `ccam lanes pipeline` uses), so lanes created before the picker shipped don't need the terminal. Both ship-feature skills now force their own template before their first `ccam stage` call, so the human never has to pick correctly at lane creation.
This commit is contained in:
@@ -132,6 +132,9 @@ export function Workspace() {
|
||||
const [addLaneOpen, setAddLaneOpen] = useState(false);
|
||||
const [viewedFeatureSlug, setViewedFeatureSlug] = useState<string | null>(null);
|
||||
const [features, setFeatures] = useState<LaneFeature[]>([]);
|
||||
const [pipelineTemplates, setPipelineTemplates] = useState<
|
||||
{ id: string; name: string; nodes: { id: string }[] }[]
|
||||
>([]);
|
||||
const [viewedFeature, setViewedFeature] = useState<LaneFeature | null>(null);
|
||||
const [proofFeatures, setProofFeatures] = useState<ProofFeature[]>([]);
|
||||
|
||||
@@ -202,7 +205,10 @@ export function Workspace() {
|
||||
api.run
|
||||
.binary()
|
||||
.then(setBinaryStatus)
|
||||
.catch(() => setBinaryStatus({ found: false, path: null }));
|
||||
// Fetch failure (server unreachable, proxy misrouted, etc.) isn't proof
|
||||
// `claude` is missing from PATH — leave the probe unresolved rather than
|
||||
// showing a misleading "claude missing" banner for an unrelated fault.
|
||||
.catch(() => undefined);
|
||||
api.run
|
||||
.list()
|
||||
.then(setActiveRuns)
|
||||
@@ -211,6 +217,10 @@ export function Workspace() {
|
||||
.history(50)
|
||||
.then((r) => setRunHistory(r.items))
|
||||
.catch(() => undefined);
|
||||
api.lanes
|
||||
.pipelines()
|
||||
.then((r) => setPipelineTemplates(r.pipelines))
|
||||
.catch(() => undefined);
|
||||
void refreshLanes();
|
||||
api.run
|
||||
.cwds()
|
||||
@@ -856,6 +866,28 @@ export function Workspace() {
|
||||
}
|
||||
};
|
||||
|
||||
// Mirrors `ccam lanes pipeline <template> <id>`: same PATCH, same
|
||||
// stage-mismatch warning when the current declared stage matches no node
|
||||
// in the newly chosen template.
|
||||
const handlePipelineChange = async (id: number, pipeline: string) => {
|
||||
setLaneActionError(null);
|
||||
try {
|
||||
const { lane } = await api.lanes.update(id, { pipeline });
|
||||
await refreshLanes();
|
||||
if (!lane.pipeline_nodes.some((n) => n.state === "current")) {
|
||||
setLaneActionError(
|
||||
tLanes("pipelinePicker.stageMismatch", {
|
||||
stage: lane.stage,
|
||||
pipeline: lane.pipeline,
|
||||
nodes: lane.pipeline_nodes.map((n) => n.id).join(", "),
|
||||
})
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
setLaneActionError(err instanceof Error ? err.message : tLanes("actionErrorUnknown"));
|
||||
}
|
||||
};
|
||||
|
||||
const consoleSection = (
|
||||
<>
|
||||
{/* Always attached under the pipeline - no header, no collapse. The
|
||||
@@ -1036,7 +1068,28 @@ export function Workspace() {
|
||||
<span className="truncate text-sm font-semibold text-fg-primary">
|
||||
{currentLane.title || currentLane.cwd}
|
||||
</span>
|
||||
<span className="text-[11px] text-fg-muted">{currentLane.pipeline_name}</span>
|
||||
<select
|
||||
data-testid="pipeline-picker"
|
||||
aria-label={tLanes("pipelinePicker.label")}
|
||||
className="rounded border border-border bg-surface-1 px-2 py-0.5 text-xs text-fg-secondary disabled:opacity-60"
|
||||
value={currentLane.pipeline}
|
||||
disabled={!!viewedFeature}
|
||||
title={
|
||||
viewedFeature
|
||||
? tLanes("features.viewingArchived", { slug: viewedFeature.slug })
|
||||
: undefined
|
||||
}
|
||||
onChange={(e) => void handlePipelineChange(currentLane.id, e.target.value)}
|
||||
>
|
||||
{(pipelineTemplates.length
|
||||
? pipelineTemplates
|
||||
: [{ id: currentLane.pipeline, name: currentLane.pipeline_name, nodes: [] }]
|
||||
).map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.nodes.length ? `${p.name} (${p.nodes.length})` : p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{/* `stage` defaults to the DB sentinel "idle" until the driving
|
||||
session ever calls `ccam stage` — that string collides with
|
||||
`status`'s own "idle"/"running" vocabulary, so a lane that is
|
||||
|
||||
Reference in New Issue
Block a user