From 8fcef5a10bad3be15e64846b3369ad3f5d3867f5 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Fri, 7 Aug 2026 09:44:48 +0700 Subject: [PATCH] feat(lanes): let Add Lane choose the pipeline template MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Creation is the only point the UI could ever set a lane's template, and it never offered the choice — so every lane added from "+ Add lane" was born on `default` and rendered an 8-node map for a 16-node workflow, with no screen able to change it afterwards. That is the defect that made the ship-feature template unreachable from the browser. The modal now shows a *Pipeline template* select fed by `GET /api/lanes/pipelines`, labelled with each template's node count so the consequence of the choice is visible. A failed fetch degrades to a `default` option rather than blocking lane creation. `pipeline` was already accepted by `POST /api/lanes` but silently dropped by `/ensure` and `/worktree`, which build their own createLane payloads; both now pass it through, and both map `EBADPIPELINE` to 400 like `EBADCWD`. --- client/src/components/lanes/AddLaneModal.tsx | 53 ++++++++++++++++- .../lanes/__tests__/AddLaneModal.test.tsx | 59 +++++++++++++++++++ client/src/i18n/locales/en/lanes.json | 2 + client/src/i18n/locales/vi/lanes.json | 2 + client/src/lib/api.ts | 12 +++- docs/API.md | 7 ++- docs/LANES.md | 4 +- server/routes/lanes.js | 11 +++- 8 files changed, 143 insertions(+), 7 deletions(-) diff --git a/client/src/components/lanes/AddLaneModal.tsx b/client/src/components/lanes/AddLaneModal.tsx index a9e04aa..1e76587 100644 --- a/client/src/components/lanes/AddLaneModal.tsx +++ b/client/src/components/lanes/AddLaneModal.tsx @@ -92,6 +92,8 @@ export function AddLaneModal({ const [branch, setBranch] = useState(""); const [branches, setBranches] = useState(null); const [base, setBase] = useState(""); + const [pipeline, setPipeline] = useState("default"); + const [pipelines, setPipelines] = useState<{ id: string; name: string; nodes: unknown[] }[]>([]); const [branchesError, setBranchesError] = useState(null); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); @@ -109,12 +111,35 @@ export function AddLaneModal({ setBranch(""); setBranches(null); setBase(""); + setPipeline("default"); setBranchesError(null); setError(null); setBusy(false); setSetupResult(null); }; + // The template a lane is created with is the ONLY chance to get it right + // from here: nothing else in the UI can change it afterwards, so a lane + // silently born on `default` renders an 8-node map for a 16-node workflow. + // Fetched on open (templates are file-backed and can change between opens). + useEffect(() => { + if (!open) return; + let cancelled = false; + api.lanes + .pipelines() + .then((r) => { + if (!cancelled) setPipelines(r.pipelines); + }) + .catch(() => { + // Quiet: the select just falls back to the single `default` option + // below, and the lane still gets created. + if (!cancelled) setPipelines([]); + }); + return () => { + cancelled = true; + }; + }, [open]); + // Look up the repo's branches once the path settles - debounced so every // keystroke while typing a path doesn't fire a request against a path that // isn't finished yet. Worktree mode only: "Repo" mode adopts as-is and @@ -158,7 +183,11 @@ export function AddLaneModal({ if (mode === "repo") { try { - const result = await api.lanes.ensure({ cwd: repo, title: name || undefined }); + const result = await api.lanes.ensure({ + cwd: repo, + title: name || undefined, + pipeline, + }); onAdded(result.lane); reset(); onClose(); @@ -176,6 +205,7 @@ export function AddLaneModal({ title: name, base: base || undefined, branch: branch.trim(), + pipeline, }); onAdded(result.lane); @@ -318,6 +348,27 @@ export function AddLaneModal({ /> +
+ + +

{t("addLanePipelineHint")}

+
+ {mode === "worktree" && branches && (