feat(lanes): pick Claude Code or Codex when adding a lane
This commit is contained in:
@@ -24,7 +24,7 @@ import { CwdAutocomplete } from "../run/RunSetup";
|
|||||||
import { FolderBrowseModal } from "./FolderBrowseModal";
|
import { FolderBrowseModal } from "./FolderBrowseModal";
|
||||||
import { api } from "../../lib/api";
|
import { api } from "../../lib/api";
|
||||||
import type { CwdSuggestion } from "../../lib/api";
|
import type { CwdSuggestion } from "../../lib/api";
|
||||||
import type { Lane } from "../../lib/types";
|
import type { Lane, LaneTool } from "../../lib/types";
|
||||||
|
|
||||||
/** Polls the lane until the background `git worktree add` finishes (status
|
/** Polls the lane until the background `git worktree add` finishes (status
|
||||||
* leaves "provisioning"), or gives up after `timeoutMs`. Returns the final
|
* leaves "provisioning"), or gives up after `timeoutMs`. Returns the final
|
||||||
@@ -95,6 +95,7 @@ export function AddLaneModal({
|
|||||||
const [pipeline, setPipeline] = useState("default");
|
const [pipeline, setPipeline] = useState("default");
|
||||||
const [pipelines, setPipelines] = useState<{ id: string; name: string; nodes: unknown[] }[]>([]);
|
const [pipelines, setPipelines] = useState<{ id: string; name: string; nodes: unknown[] }[]>([]);
|
||||||
const [branchesError, setBranchesError] = useState<string | null>(null);
|
const [branchesError, setBranchesError] = useState<string | null>(null);
|
||||||
|
const [tool, setTool] = useState<LaneTool>("claude");
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [browseOpen, setBrowseOpen] = useState(false);
|
const [browseOpen, setBrowseOpen] = useState(false);
|
||||||
@@ -112,6 +113,7 @@ export function AddLaneModal({
|
|||||||
setBranches(null);
|
setBranches(null);
|
||||||
setBase("");
|
setBase("");
|
||||||
setPipeline("default");
|
setPipeline("default");
|
||||||
|
setTool("claude");
|
||||||
setBranchesError(null);
|
setBranchesError(null);
|
||||||
setError(null);
|
setError(null);
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
@@ -187,6 +189,7 @@ export function AddLaneModal({
|
|||||||
cwd: repo,
|
cwd: repo,
|
||||||
title: name || undefined,
|
title: name || undefined,
|
||||||
pipeline,
|
pipeline,
|
||||||
|
tool,
|
||||||
});
|
});
|
||||||
onAdded(result.lane);
|
onAdded(result.lane);
|
||||||
reset();
|
reset();
|
||||||
@@ -206,6 +209,7 @@ export function AddLaneModal({
|
|||||||
base: base || undefined,
|
base: base || undefined,
|
||||||
branch: branch.trim(),
|
branch: branch.trim(),
|
||||||
pipeline,
|
pipeline,
|
||||||
|
tool,
|
||||||
});
|
});
|
||||||
onAdded(result.lane);
|
onAdded(result.lane);
|
||||||
|
|
||||||
@@ -307,6 +311,22 @@ export function AddLaneModal({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="mb-1 block text-xs text-fg-secondary">{t("addLaneToolHint")}</label>
|
||||||
|
<div className="flex items-center rounded-md border border-border bg-surface-2 p-0.5">
|
||||||
|
<Seg
|
||||||
|
active={tool === "claude"}
|
||||||
|
label={t("addLaneToolClaude")}
|
||||||
|
onClick={() => setTool("claude")}
|
||||||
|
/>
|
||||||
|
<Seg
|
||||||
|
active={tool === "codex"}
|
||||||
|
label={t("addLaneToolCodex")}
|
||||||
|
onClick={() => setTool("codex")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="mb-1 block text-xs text-fg-secondary" htmlFor="add-lane-repo">
|
<label className="mb-1 block text-xs text-fg-secondary" htmlFor="add-lane-repo">
|
||||||
{mode === "repo" ? t("addLaneRepoLabelAdopt") : t("addLaneRepoLabel")}
|
{mode === "repo" ? t("addLaneRepoLabelAdopt") : t("addLaneRepoLabel")}
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ describe("AddLaneModal — worktree mode (default)", () => {
|
|||||||
base: "main",
|
base: "main",
|
||||||
branch: "feat/new-feature",
|
branch: "feat/new-feature",
|
||||||
pipeline: "default",
|
pipeline: "default",
|
||||||
|
tool: "claude",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
expect(onAdded).toHaveBeenCalledWith(
|
expect(onAdded).toHaveBeenCalledWith(
|
||||||
@@ -346,6 +347,7 @@ describe("AddLaneModal — repo mode (adopt)", () => {
|
|||||||
cwd: "/Users/tester/projects/repo",
|
cwd: "/Users/tester/projects/repo",
|
||||||
title: "main repo",
|
title: "main repo",
|
||||||
pipeline: "default",
|
pipeline: "default",
|
||||||
|
tool: "claude",
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(api.lanes.worktree).not.toHaveBeenCalled();
|
expect(api.lanes.worktree).not.toHaveBeenCalled();
|
||||||
@@ -439,3 +441,48 @@ describe("AddLaneModal — pipeline template", () => {
|
|||||||
expect(screen.getByRole("option", { name: "default" })).toBeInTheDocument();
|
expect(screen.getByRole("option", { name: "default" })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("AddLaneModal — tool selection", () => {
|
||||||
|
it("sends the picked tool when adopting a repo", async () => {
|
||||||
|
vi.mocked(api.lanes.ensure).mockResolvedValue({
|
||||||
|
lane: laneFixture({ id: 23, kind: "adopted", status: "idle" }),
|
||||||
|
created: true,
|
||||||
|
});
|
||||||
|
const onAdded = vi.fn();
|
||||||
|
const onClose = vi.fn();
|
||||||
|
renderModal({ onAdded, onClose });
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Repo" }));
|
||||||
|
const dirField = screen.getByLabelText("Directory");
|
||||||
|
await focusField(user, dirField);
|
||||||
|
await user.type(dirField, "/tmp/demo");
|
||||||
|
await user.click(screen.getByRole("button", { name: /codex/i }));
|
||||||
|
await user.click(screen.getByRole("button", { name: /add lane|create/i }));
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(api.lanes.ensure).toHaveBeenCalledWith(expect.objectContaining({ tool: "codex" }))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults to claude code", async () => {
|
||||||
|
vi.mocked(api.lanes.ensure).mockResolvedValue({
|
||||||
|
lane: laneFixture({ id: 24, kind: "adopted", status: "idle" }),
|
||||||
|
created: true,
|
||||||
|
});
|
||||||
|
const onAdded = vi.fn();
|
||||||
|
const onClose = vi.fn();
|
||||||
|
renderModal({ onAdded, onClose });
|
||||||
|
const user = userEvent.setup();
|
||||||
|
|
||||||
|
await user.click(screen.getByRole("button", { name: "Repo" }));
|
||||||
|
const dirField = screen.getByLabelText("Directory");
|
||||||
|
await focusField(user, dirField);
|
||||||
|
await user.type(dirField, "/tmp/demo");
|
||||||
|
await user.click(screen.getByRole("button", { name: /add lane|create/i }));
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(api.lanes.ensure).toHaveBeenCalledWith(expect.objectContaining({ tool: "claude" }))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -37,6 +37,9 @@
|
|||||||
"addLaneSetupProfile": "Profile",
|
"addLaneSetupProfile": "Profile",
|
||||||
"addLaneSetupTitle": "Setup",
|
"addLaneSetupTitle": "Setup",
|
||||||
"addLaneTitleLabel": "Title",
|
"addLaneTitleLabel": "Title",
|
||||||
|
"addLaneToolClaude": "Claude Code",
|
||||||
|
"addLaneToolCodex": "Codex",
|
||||||
|
"addLaneToolHint": "Which CLI drives this lane. Cannot be changed later.",
|
||||||
"mode.repo": "Repo",
|
"mode.repo": "Repo",
|
||||||
"mode.repoHint": "Adopt this directory as a lane, as-is — no worktree, no new branch.",
|
"mode.repoHint": "Adopt this directory as a lane, as-is — no worktree, no new branch.",
|
||||||
"mode.worktree": "Worktree",
|
"mode.worktree": "Worktree",
|
||||||
|
|||||||
@@ -37,6 +37,9 @@
|
|||||||
"addLaneSetupProfile": "Profile",
|
"addLaneSetupProfile": "Profile",
|
||||||
"addLaneSetupTitle": "Thiết lập",
|
"addLaneSetupTitle": "Thiết lập",
|
||||||
"addLaneTitleLabel": "Tiêu đề",
|
"addLaneTitleLabel": "Tiêu đề",
|
||||||
|
"addLaneToolClaude": "Claude Code",
|
||||||
|
"addLaneToolCodex": "Codex",
|
||||||
|
"addLaneToolHint": "CLI nào điều khiển lane này. Không thể thay đổi sau này.",
|
||||||
"mode.repo": "Repo",
|
"mode.repo": "Repo",
|
||||||
"mode.repoHint": "Gắn thẳng thư mục này làm lane, nguyên trạng — không worktree, không nhánh mới.",
|
"mode.repoHint": "Gắn thẳng thư mục này làm lane, nguyên trạng — không worktree, không nhánh mới.",
|
||||||
"mode.worktree": "Worktree",
|
"mode.worktree": "Worktree",
|
||||||
|
|||||||
Reference in New Issue
Block a user