feat(lanes): let Add Lane choose the pipeline template

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`.
This commit is contained in:
2026-08-07 09:44:48 +07:00
parent 67edda77eb
commit 8fcef5a10b
8 changed files with 143 additions and 7 deletions
+8 -3
View File
@@ -161,11 +161,15 @@ router.post("/ensure", sameOriginGuard, (req, res) => {
const owner = lanesLib.resolveLaneByCwd(body.cwd);
if (owner) return res.json({ lane: payload(owner), created: false });
try {
const lane = lanesLib.createLane({ cwd: body.cwd, title: body.title || "" });
const lane = lanesLib.createLane({
cwd: body.cwd,
title: body.title || "",
...(body.pipeline ? { pipeline: body.pipeline } : {}),
});
broadcastLane(lane.id);
return res.status(201).json({ lane: payload(lane), created: true });
} catch (err) {
if (err.code === "EBADCWD") {
if (err.code === "EBADCWD" || err.code === "EBADPIPELINE") {
return res.status(400).json({ error: { code: err.code, message: err.message } });
}
// The cwd UNIQUE constraint is the arbiter: someone else won the race, so
@@ -248,7 +252,7 @@ router.post("/", sameOriginGuard, (req, res) => {
broadcastLane(lane.id);
res.status(201).json({ lane: payload(lane) });
} catch (err) {
if (err.code === "EBADCWD") {
if (err.code === "EBADCWD" || err.code === "EBADPIPELINE" || err.code === "EBADKIND") {
return res.status(400).json({ error: { code: err.code, message: err.message } });
}
if (err.code === "SQLITE_CONSTRAINT_UNIQUE" || String(err.message).includes("UNIQUE")) {
@@ -569,6 +573,7 @@ router.post("/worktree", sameOriginGuard, async (req, res) => {
source_repo: resolvedSourceRepo,
base_branch: body.base || null,
slug,
...(body.pipeline ? { pipeline: body.pipeline } : {}),
});
lane = lanesLib.updateLane(lane.id, { status: "provisioning" });
} catch (err) {