feat(lanes): drive runs from the pipeline template's own skill
A pipeline template may now declare the slash command that drives it (`skill`), so the choice of pipeline is the choice of workflow rather than just a picture on the dashboard: - `ccam ship-full "<req>"` runs the 16-node `ship-feature` template's skill (`/ccam:ship-feature-lane`) — through gates, QC and the senior gate to a published PR, watched until a human merges it. - `ccam ship-quick "<req>"` runs the 8-node `default` template's skill (`/ccam:ship-feature`) — in-repo feature work that stops at the gate. Each command puts the lane on its own template before spawning, so the dashboard renders the right node count from the first stage instead of after the skill self-corrects. `--print` shows the command, switches nothing and runs nothing. The prompt reaches `claude` as one argv element, never through a shell. The same field feeds the Workspace run console, which pre-fills its prompt with the lane's driving skill and never overwrites a prompt the user has already edited.
This commit is contained in:
@@ -121,6 +121,7 @@ export function LaneConsolePane({
|
||||
useEffect(() => {
|
||||
const { activeRuns: runs } = latest.current;
|
||||
setPrompt("");
|
||||
lastPrefill.current = "";
|
||||
setResumeSession(null);
|
||||
setError(null);
|
||||
setBusy(null);
|
||||
@@ -129,6 +130,23 @@ export function LaneConsolePane({
|
||||
refreshList();
|
||||
}, [laneId, refreshList]);
|
||||
|
||||
// Pre-fill the prompt with the slash command the lane's pipeline template
|
||||
// names as its driver, so picking the 16-node pipeline starts the 16-step
|
||||
// skill and the 8-node one the 8-step skill. Only overwrites a prompt the
|
||||
// user hasn't touched (still exactly the previous prefill), so switching
|
||||
// templates never eats a typed requirement. Must be declared AFTER the
|
||||
// lane-switch effect: on a lane change that one clears both first.
|
||||
const lastPrefill = useRef("");
|
||||
const pipelineSkill = currentLane?.pipeline_skill || "";
|
||||
useEffect(() => {
|
||||
const next = pipelineSkill ? `${pipelineSkill} ` : "";
|
||||
setPrompt((p) => (p === lastPrefill.current ? next : p));
|
||||
lastPrefill.current = next;
|
||||
// `laneId` is a dep too: switching between two lanes on the SAME template
|
||||
// leaves `pipelineSkill` unchanged, and the switch effect just cleared the
|
||||
// prompt — without it the new lane would open with an empty box.
|
||||
}, [pipelineSkill, laneId]);
|
||||
|
||||
// The switch effect above only sees whatever `activeRuns` the page already
|
||||
// had loaded at that instant. Remounting this page (navigating away and
|
||||
// back) starts `activeRuns` at null again, so a lane with a live run would
|
||||
|
||||
@@ -188,6 +188,24 @@ describe("LaneConsolePane", () => {
|
||||
expect(screen.queryByTestId("pane-empty")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("pre-fills the prompt with the pipeline's driving skill, and keeps a typed prompt when the template changes", async () => {
|
||||
const lane = { ...LANE, pipeline_skill: "/ccam:ship-feature" };
|
||||
const props = { ...baseProps(), lanes: [lane] };
|
||||
const { rerender } = render(<LaneConsolePane {...props} />);
|
||||
const prompt = screen.getByPlaceholderText(/ask claude/i) as HTMLTextAreaElement;
|
||||
await waitFor(() => expect(prompt.value).toBe("/ccam:ship-feature "));
|
||||
|
||||
// Untouched prefill follows the template switch.
|
||||
const laneShip = { ...lane, pipeline_skill: "/ccam:ship-feature-lane" };
|
||||
rerender(<LaneConsolePane {...props} lanes={[laneShip]} />);
|
||||
await waitFor(() => expect(prompt.value).toBe("/ccam:ship-feature-lane "));
|
||||
|
||||
// A typed requirement is never eaten by a later template switch.
|
||||
fireEvent.change(prompt, { target: { value: "/ccam:ship-feature-lane add a button" } });
|
||||
rerender(<LaneConsolePane {...props} lanes={[lane]} />);
|
||||
await waitFor(() => expect(prompt.value).toBe("/ccam:ship-feature-lane add a button"));
|
||||
});
|
||||
|
||||
it("renders an empty placeholder with selector when laneId is null but showLaneSelector is true (split-view)", () => {
|
||||
render(<LaneConsolePane {...baseProps()} laneId={null} showLaneSelector={true} />);
|
||||
expect(screen.getByTestId("pane-empty")).toBeInTheDocument();
|
||||
|
||||
Reference in New Issue
Block a user