feat(lanes): surface child worktrees on a lane card + detect Superpowers skills

Adopting the main repo as its own lane now gets stage detection
(cwd matches, same as any other lane), and its card lists every
managed-worktree lane provisioned from it with a jump-to link.
Also add the two missing Skill-tool detect rules (implement, ship)
so detection covers all four Superpowers workflow phases, not just
plan/review.
This commit is contained in:
2026-08-06 15:33:07 +07:00
parent 54299f119e
commit 9f13769fb4
9 changed files with 98 additions and 0 deletions
+33
View File
@@ -180,9 +180,17 @@ function since(sec: number | null): string {
export default function LaneCard({
lane,
onAction,
childWorktrees,
onSelectLane,
}: {
lane: Lane;
onAction: (action: string, body?: Record<string, unknown>) => void;
/** Other lanes whose `source_repo` is this lane's `cwd` — populated only
* when this lane is itself a source repo (typically an adopted one) that
* other lanes were provisioned as worktrees from. */
childWorktrees?: Lane[];
/** Jumps the Workspace page's selection to another lane's card. */
onSelectLane?: (id: number) => void;
}) {
const { t } = useTranslation(["lanes"]);
const [destructiveAction, setDestructiveAction] = useState<"reset" | "remove" | "purge" | null>(
@@ -452,6 +460,31 @@ export default function LaneCard({
</div>
</dl>
{childWorktrees && childWorktrees.length > 0 && (
<div
data-testid="lane-child-worktrees"
className="mb-3 space-y-1 text-[11px] text-fg-secondary"
>
<div className="text-fg-muted">
{t("worktrees.heading", { count: childWorktrees.length })}
</div>
<ul className="space-y-0.5">
{childWorktrees.map((w) => (
<li key={w.id}>
<button
type="button"
onClick={() => onSelectLane?.(w.id)}
className="truncate text-left text-blue-400 hover:underline"
title={w.cwd}
>
#{w.id} {w.title || w.cwd} · {w.status}
</button>
</li>
))}
</ul>
</div>
)}
{integrations && (
<div className="mb-2 flex items-center gap-1.5 text-[10px]">
{(["tracker", "dev_qc", "ci_wait"] as const).map((name) => (
@@ -37,6 +37,7 @@ function laneFixture(over: Partial<Lane> = {}): Lane {
cwd: "/lanes/repo__feature",
branch: "feat/feature",
kind: "managed",
source_repo: null,
pipeline: "default",
session_id: null,
run_id: null,
@@ -54,6 +54,7 @@ function makeLane(overrides: Partial<Lane> = {}): Lane {
cwd: "/work/demo",
branch: "lane/demo",
kind: "adopted",
source_repo: null,
pipeline: "default",
session_id: null,
run_id: null,
@@ -90,6 +91,31 @@ describe("LaneCard status badge", () => {
}
});
describe("LaneCard child worktrees", () => {
it("lists worktrees provisioned from this lane and jumps to one on click", async () => {
const onSelectLane = vi.fn();
const worktree = makeLane({ id: 8, title: "Worktree A", status: "running" });
render(
<LaneCard
lane={makeLane({ id: 1 })}
onAction={vi.fn()}
childWorktrees={[worktree]}
onSelectLane={onSelectLane}
/>
);
expect(screen.getByTestId("lane-child-worktrees")).toBeInTheDocument();
const user = userEvent.setup();
await user.click(screen.getByRole("button", { name: /Worktree A/ }));
expect(onSelectLane).toHaveBeenCalledWith(8);
});
it("renders nothing when there are no child worktrees", () => {
render(<LaneCard lane={makeLane({ id: 1 })} onAction={vi.fn()} />);
expect(screen.queryByTestId("lane-child-worktrees")).not.toBeInTheDocument();
});
});
const pipelineNodes: Lane["pipeline_nodes"] = [
{ id: "intake", label: "intake", icon: "📥", gate: false, state: "done" },
{ id: "plan", label: "plan", icon: "🧭", gate: false, state: "done" },
+2
View File
@@ -29,6 +29,8 @@
"addLaneSetupProfile": "Profile",
"addLaneSetupTitle": "Setup",
"addLaneTitleLabel": "Title",
"worktrees.heading_one": "{{count}} worktree",
"worktrees.heading_other": "{{count}} worktrees",
"addLaneTitlePlaceholder": "Optional",
"autoStage": "auto: {{stage}}",
"cardId": "Lane {{id}}",
+2
View File
@@ -29,6 +29,8 @@
"addLaneSetupProfile": "Profile",
"addLaneSetupTitle": "Thiết lập",
"addLaneTitleLabel": "Tiêu đề",
"worktrees.heading_one": "{{count}} worktree",
"worktrees.heading_other": "{{count}} worktree",
"addLaneTitlePlaceholder": "Không bắt buộc",
"autoStage": "tự động: {{stage}}",
"cardId": "Làn đường {{id}}",
+3
View File
@@ -2319,6 +2319,9 @@ export interface Lane {
cwd: string;
branch: string | null;
kind: "managed" | "adopted";
/** For a managed worktree lane, the repo it was provisioned from. Null for
* an adopted lane (it IS a source repo, not a worktree of one). */
source_repo: string | null;
pipeline: string;
session_id: string | null;
run_id: string | null;
+4
View File
@@ -1079,6 +1079,10 @@ export function Workspace() {
<LaneCard
lane={currentLane}
onAction={(a, b) => handleLaneAction(currentLane.id, a, b)}
childWorktrees={lanes.filter(
(l) => l.source_repo === currentLane.cwd && l.id !== currentLane.id
)}
onSelectLane={setSelectedLaneId}
/>
</div>
<div className="mb-3">