Skip to content

Commit 21d5c2c

Browse files
quickbeardclaude
andauthored
feat(configure): surface Codex and OpenCode in codevhub install / config (#263)
Revert the temporary hide from #223. The Codex and OpenCode rows return to ToolSelect, the single picker shared by `codevhub install` and `codevhub config`, so new users can select and configure them again. Nothing downstream changes: the Tool union, configureCodex / configureOpenCode, detection, restore, update and the launch commands were never touched by the hide. This removes the `hidden` flag and the derived VISIBLE_TOOLS list, since TOOLS is once again the rendered set. Why now: `codevhub readiness` (#197) offers Claude Code, Codex and OpenCode, but gates each on the agent being CoDev-configured and tells the user to "run `codevhub install`, select this agent". With the rows hidden, that instruction was impossible to follow for anyone who installed after #223 — the OpenCode readiness path and its `--agent opencode` flag were unreachable. Tests: restore the extension-row navigation counts (rows shift down two positions), replace the "hides Codex and OpenCode" assertion with the positive render check, and un-skip the two Codex-selection integration tests in InstallApp.test.tsx — they pass unchanged. Claude-Session: https://claude.ai/code/session_0178nPaQqSGVe7Y1fR2ovDTK Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 66962d9 commit 21d5c2c

3 files changed

Lines changed: 47 additions & 66 deletions

File tree

‎src/components/ToolSelect.tsx‎

Lines changed: 9 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -14,36 +14,23 @@ export type ToolSelectSentinel =
1414
| typeof CONTINUE_SENTINEL;
1515
export type ToolSelectValue = Tool | ToolSelectSentinel;
1616

17-
const TOOLS: {
18-
label: string;
19-
value: ToolSelectValue;
20-
locked?: boolean;
21-
hidden?: boolean;
22-
}[] = [
17+
const TOOLS: { label: string; value: ToolSelectValue; locked?: boolean }[] = [
2318
// CoDev Code is the flagship agent — always installed and configured, so its
2419
// row is shown pre-checked and can't be toggled off. Kept at index 0 so the
2520
// optional agents keep their positions.
2621
{ label: "CoDev Code", value: "codev-code", locked: true },
2722
{ label: "Claude Code", value: "claude-code" },
28-
// Codex and OpenCode are temporarily withheld from the selection UI. All the
29-
// underlying configure/install/update logic still handles them end-to-end —
30-
// they're only hidden from users until we're ready to surface them. Flip
31-
// `hidden` off (or delete it) to bring the rows back.
32-
{ label: "Codex", value: "codex", hidden: true },
33-
{ label: "OpenCode", value: "opencode", hidden: true },
23+
{ label: "Codex", value: "codex" },
24+
{ label: "OpenCode", value: "opencode" },
3425
{ label: "Claude Code (extension)", value: CLAUDE_CODE_EXT_SENTINEL },
3526
{ label: "Continue (extension)", value: CONTINUE_SENTINEL },
3627
];
3728

38-
// Rows actually rendered and navigable. Hidden tools are dropped from the UI
39-
// only; everything downstream (Configure, restore, update) is untouched.
40-
const VISIBLE_TOOLS = TOOLS.filter((t) => !t.hidden);
41-
4229
// Locked tools are emitted on every confirm regardless of the mutable
4330
// selection, and always lead the emitted list.
44-
const LOCKED_VALUES: ToolSelectValue[] = VISIBLE_TOOLS.filter(
45-
(t) => t.locked,
46-
).map((t) => t.value);
31+
const LOCKED_VALUES: ToolSelectValue[] = TOOLS.filter((t) => t.locked).map(
32+
(t) => t.value,
33+
);
4734

4835
interface ToolSelectProps {
4936
onConfirm: (tools: ToolSelectValue[]) => void;
@@ -64,9 +51,9 @@ export function ToolSelect({
6451
if (key.upArrow) {
6552
setCursor((c) => Math.max(0, c - 1));
6653
} else if (key.downArrow) {
67-
setCursor((c) => Math.min(VISIBLE_TOOLS.length - 1, c + 1));
54+
setCursor((c) => Math.min(TOOLS.length - 1, c + 1));
6855
} else if (input === " ") {
69-
const tool = VISIBLE_TOOLS[cursor];
56+
const tool = TOOLS[cursor];
7057
// Locked rows (CoDev Code) are always included and can't be toggled.
7158
if (!tool || tool.locked) return;
7259
setSelected((prev) => {
@@ -92,7 +79,7 @@ export function ToolSelect({
9279

9380
return (
9481
<Box flexDirection="column">
95-
{VISIBLE_TOOLS.map((tool, i) => {
82+
{TOOLS.map((tool, i) => {
9683
const isSelected = tool.locked || selected.has(tool.value);
9784
const isCursor = !readOnly && cursor === i;
9885
return (

‎tests/InstallApp.test.tsx‎

Lines changed: 11 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -531,9 +531,7 @@ describe("InstallApp fail-stop invariant", () => {
531531
expect(history).toContain("Happy coding");
532532
});
533533

534-
// Parked while Codex is hidden from ToolSelect (still fully wired downstream —
535-
// just not user-selectable). Un-skip when the Codex row is surfaced again.
536-
test.skip("Codex selection routes to configureCodex and reaches done", async () => {
534+
test("Codex selection routes to configureCodex and reaches done", async () => {
537535
stubExecFile(() => ({ stdout: "ok" }));
538536
stubModels();
539537
vi.spyOn(auth, "login").mockResolvedValue(fakeAuth());
@@ -1093,10 +1091,9 @@ describe("InstallApp fail-stop invariant", () => {
10931091

10941092
const { stdin, frames } = render(<InstallApp />);
10951093

1096-
// Pick the Claude Code (extension) row (3rd, index 2 — Codex/OpenCode
1097-
// are hidden, so it sits right below Claude Code).
1094+
// Pick the Claude Code (extension) row (5th, index 4).
10981095
await waitForFrame(frames, "Select the AI agent(s) to install");
1099-
for (let i = 0; i < 2; i++) {
1096+
for (let i = 0; i < 4; i++) {
11001097
stdin.write("\x1B[B");
11011098
await new Promise((r) => setTimeout(r, 30));
11021099
}
@@ -1126,9 +1123,8 @@ describe("InstallApp fail-stop invariant", () => {
11261123
});
11271124

11281125
test("Claude Code CLI + extension share the backup kind: single configure call, both install tasks scheduled", async () => {
1129-
// Picks Claude Code CLI (2nd row) AND Claude Code (extension) (3rd
1130-
// row — Codex/OpenCode are hidden), then VS Code in the merged sub-
1131-
// select. Asserts:
1126+
// Picks Claude Code CLI (2nd row) AND Claude Code (extension) (5th
1127+
// row), then VS Code in the merged sub-select. Asserts:
11321128
// - `configureClaudeCode` runs exactly once (shared BackupKind).
11331129
// - Both the npm install task (@anthropic-ai/claude-code) and the
11341130
// extension install task (anthropic.claude-code (VS Code)) appear.
@@ -1157,14 +1153,15 @@ describe("InstallApp fail-stop invariant", () => {
11571153
const { stdin, frames } = render(<InstallApp />);
11581154

11591155
await waitForFrame(frames, "Select the AI agent(s) to install");
1160-
// Row 1 (Claude Code CLI) — toggle, then arrow down to row 2 (Claude
1161-
// Code (extension), now directly below) and toggle.
1156+
// Row 1 (Claude Code CLI) — toggle, then arrow down to row 4 and toggle.
11621157
stdin.write("\x1B[B");
11631158
await new Promise((r) => setTimeout(r, 30));
11641159
stdin.write(" ");
11651160
await new Promise((r) => setTimeout(r, 30));
1166-
stdin.write("\x1B[B");
1167-
await new Promise((r) => setTimeout(r, 30));
1161+
for (let i = 0; i < 3; i++) {
1162+
stdin.write("\x1B[B");
1163+
await new Promise((r) => setTimeout(r, 30));
1164+
}
11681165
stdin.write(" ");
11691166
await new Promise((r) => setTimeout(r, 30));
11701167
stdin.write("\r");
@@ -1230,10 +1227,7 @@ describe("InstallApp fail-stop invariant", () => {
12301227
});
12311228
});
12321229

1233-
// Parked while Codex is hidden from ToolSelect: this exercises the
1234-
// survivor-advances fail-stop via a Codex selection that's no longer
1235-
// user-reachable. Un-skip when the Codex row is surfaced again.
1236-
test.skip("partial install failure: survivor advances to Configure, failed tool is dropped", async () => {
1230+
test("partial install failure: survivor advances to Configure, failed tool is dropped", async () => {
12371231
// User selects both Claude Code and Codex. The codex npm install
12381232
// hard-fails ("disk full"); the claude-code one succeeds. Pre-change
12391233
// behavior was to park at install-failed and force the user to Ctrl-C.

‎tests/components/ToolSelect.test.tsx‎

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,19 @@ afterEach(() => {
77
});
88

99
describe("ToolSelect", () => {
10-
test("renders the visible tool options", () => {
10+
test("renders all tool options", () => {
1111
const onConfirm = vi.fn();
1212
const { lastFrame } = render(<ToolSelect onConfirm={onConfirm} />);
1313

1414
const output = lastFrame() ?? "";
1515
expect(output).toContain("CoDev Code");
1616
expect(output).toContain("Claude Code");
17+
expect(output).toContain("OpenCode");
18+
expect(output).toContain("Codex");
1719
expect(output).toContain("Claude Code (extension)");
1820
expect(output).toContain("Continue (extension)");
1921
});
2022

21-
test("hides Codex and OpenCode from the selection UI", () => {
22-
// Both are temporarily withheld from users — still fully wired downstream,
23-
// just not rendered or selectable here. Remove the `hidden` flag in
24-
// ToolSelect's TOOLS array to bring them back.
25-
const onConfirm = vi.fn();
26-
const { lastFrame } = render(<ToolSelect onConfirm={onConfirm} />);
27-
28-
const output = lastFrame() ?? "";
29-
expect(output).not.toContain("Codex");
30-
expect(output).not.toContain("OpenCode");
31-
});
32-
3323
test("emits the `claude-code-ext` sentinel when the Claude Code (extension) row is picked", async () => {
3424
// The extension rows are editor-agnostic; the merged editor sub-
3525
// select runs next. ToolSelect emits a sentinel that InstallApp
@@ -38,10 +28,8 @@ describe("ToolSelect", () => {
3828
const onConfirm = vi.fn();
3929
const { stdin } = render(<ToolSelect onConfirm={onConfirm} />);
4030

41-
// Two down-arrows to reach the 3rd (Claude Code (extension)) row —
42-
// Codex and OpenCode are hidden, so the extension rows sit right below
43-
// Claude Code.
44-
for (let i = 0; i < 2; i++) {
31+
// Four down-arrows to reach the 5th (Claude Code (extension)) row.
32+
for (let i = 0; i < 4; i++) {
4533
stdin.write("\x1B[B");
4634
await new Promise((r) => setTimeout(r, 50));
4735
}
@@ -57,10 +45,8 @@ describe("ToolSelect", () => {
5745
const onConfirm = vi.fn();
5846
const { stdin } = render(<ToolSelect onConfirm={onConfirm} />);
5947

60-
// Three down-arrows to reach the 4th (Continue (extension)) row —
61-
// Codex and OpenCode are hidden, so it sits directly below the Claude
62-
// Code (extension) row.
63-
for (let i = 0; i < 3; i++) {
48+
// Five down-arrows to reach the 6th (Continue (extension)) row.
49+
for (let i = 0; i < 5; i++) {
6450
stdin.write("\x1B[B");
6551
await new Promise((r) => setTimeout(r, 50));
6652
}
@@ -77,8 +63,8 @@ describe("ToolSelect", () => {
7763
const { lastFrame } = render(<ToolSelect onConfirm={onConfirm} />);
7864

7965
const output = lastFrame() ?? "";
80-
// Exactly one filled box — the always-on CoDev Code row — and the
81-
// remaining optional agents render unchecked.
66+
// Exactly one filled box — the always-on CoDev Code row — and the five
67+
// optional agents render unchecked.
8268
expect((output.match(/■/g) ?? []).length).toBe(1);
8369
expect(output).toContain("□");
8470
expect(output).toContain("(always installed)");
@@ -153,9 +139,7 @@ describe("ToolSelect", () => {
153139
const onConfirm = vi.fn();
154140
const { stdin } = render(<ToolSelect onConfirm={onConfirm} />);
155141

156-
// Down to Claude Code (row 1), select, down to Claude Code (extension)
157-
// (row 2), select. Codex/OpenCode are hidden, so the extension row is
158-
// the next selectable option below Claude Code.
142+
// Down to Claude Code (row 1), select, down to Codex (row 2), select.
159143
stdin.write("\x1B[B");
160144
await new Promise((r) => setTimeout(r, 50));
161145
stdin.write(" ");
@@ -172,10 +156,26 @@ describe("ToolSelect", () => {
172156
expect(onConfirm).toHaveBeenCalledWith([
173157
"codev-code",
174158
"claude-code",
175-
"claude-code-ext",
159+
"codex",
176160
]);
177161
});
178162

163+
test("can select Codex by moving cursor down twice", async () => {
164+
const onConfirm = vi.fn();
165+
const { stdin } = render(<ToolSelect onConfirm={onConfirm} />);
166+
167+
stdin.write("\x1B[B");
168+
await new Promise((r) => setTimeout(r, 50));
169+
stdin.write("\x1B[B");
170+
await new Promise((r) => setTimeout(r, 50));
171+
stdin.write(" ");
172+
await new Promise((r) => setTimeout(r, 50));
173+
stdin.write("\r");
174+
await new Promise((r) => setTimeout(r, 50));
175+
176+
expect(onConfirm).toHaveBeenCalledWith(["codev-code", "codex"]);
177+
});
178+
179179
test("can deselect a tool", async () => {
180180
const onConfirm = vi.fn();
181181
const { lastFrame, stdin } = render(<ToolSelect onConfirm={onConfirm} />);

0 commit comments

Comments
 (0)