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">
|
||||
|
||||
@@ -32,6 +32,8 @@ ccam lanes add --repo /path/to/repo --title "My Feature" --base main --slug my-f
|
||||
|
||||
`--title`, `--base`, and `--slug` are optional. The CLI waits for background provisioning to finish and reports either the ready lane or its failure notes.
|
||||
|
||||
Adopting your main repo (`ccam lanes add --cwd $(pwd)`) is also how you get stage detection working for sessions that work directly in it rather than in a worktree — see "Superpowers skill invocations" below. Once adopted, that lane's own card lists every managed-worktree lane whose `source_repo` matches its `cwd`, each a clickable link to jump to that lane.
|
||||
|
||||
Adding a lane through the dashboard's "+ Add lane" flow also auto-runs, best-effort, in parallel: `ccam lanes profile init` (only if a Node.js project is detected — most repos won't be, and that's a normal outcome, not a failure), `ccam lanes agents install`, and `ccam lanes mcp sync`. None of the three blocks the lane from being created or from each other — a lane whose repo has no MCP servers configured, for instance, still gets created and is still usable, just without a synced `.mcp.json`. The modal shows a ✓/✗ summary of the three results and stays open until dismissed (Cancel/X) — it does not auto-close. Run any of the three manually later (from the lane's own card, or the CLI) if the automatic attempt didn't apply.
|
||||
|
||||
## Destructive lane actions
|
||||
@@ -672,6 +674,23 @@ to the flattened input. Either way the result is capped at 120 characters
|
||||
lane's tooltip sometimes shows a file path or a skill name rather than a shell
|
||||
command: it's whichever of the fields above the matching rule's `tool` carried.
|
||||
|
||||
### Superpowers skill invocations
|
||||
|
||||
The built-in `default` pipeline's `plan`, `implement`, `review`, and `ship`
|
||||
nodes each carry a `{"tool": "Skill", "match": "..."}` rule matching the
|
||||
Superpowers workflow skill names (`brainstorming`/`writing-plans`,
|
||||
`executing-plans`/`subagent-driven-development`, `code-review`/
|
||||
`requesting-code-review`, `finishing-a-development-branch`). Invoking one of
|
||||
these skills is a much stronger signal than a matched Bash command, but it is
|
||||
still detection, not declaration — it renders dashed amber and never `done`,
|
||||
same as every other detected stage.
|
||||
|
||||
Detection only ever attributes to a lane whose `cwd` matches the hook's
|
||||
session `cwd` (see "Which lane a signal is credited to" above). A session
|
||||
working directly in a source repo that was never itself adopted as a lane —
|
||||
run `ccam lanes add --cwd $(pwd)` from that repo to fix that — gets no
|
||||
detection at all, because no lane owns that `cwd`.
|
||||
|
||||
### Detection expires
|
||||
|
||||
Forward-only would otherwise park a lane at the highest stage it ever touched:
|
||||
|
||||
@@ -45,6 +45,10 @@
|
||||
"build"
|
||||
],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Skill",
|
||||
"match": "executing-plans|subagent-driven-development"
|
||||
},
|
||||
{
|
||||
"tool": "Edit"
|
||||
},
|
||||
@@ -118,6 +122,10 @@
|
||||
"push"
|
||||
],
|
||||
"detect": [
|
||||
{
|
||||
"tool": "Skill",
|
||||
"match": "finishing-a-development-branch"
|
||||
},
|
||||
{
|
||||
"tool": "Bash",
|
||||
"match": "\\bgit\\b(?:\\s+-c\\s+[\\w.-]+=(?:'[^']*'|\\\"[^\\\"]*\\\"|\\S+)|\\s+-{1,2}[\\w.-]+(?:=(?:'[^']*'|\\\"[^\\\"]*\\\"|\\S+))?)*\\s+push\\b|\\bgh\\b[^;&|]*\\bpr create\\b"
|
||||
|
||||
Reference in New Issue
Block a user