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:
@@ -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" },
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user