From f8dfc6715d342415213f4c63311af154b8855734 Mon Sep 17 00:00:00 2001 From: minhn4 Date: Thu, 30 Jul 2026 21:02:50 +0700 Subject: [PATCH] feat(configure): hide Codex and OpenCode from the install/config TUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Withhold the Codex and OpenCode rows from ToolSelect (the single selection surface shared by `codevhub install` and `codevhub config`) via a `hidden` flag and a derived VISIBLE_TOOLS list. This hides them from users only — the Tool union, configureCodex/configureOpenCode, providers, detection, restore, update, and the passthrough launch commands are all untouched. Re-surfacing is a one-line change: drop the `hidden` flags. Tests: fix the extension-row navigation counts (the rows shift up two positions), assert Codex/OpenCode no longer render, and park the two Codex-selection integration tests with test.skip so their coverage returns intact when the rows are surfaced again. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/components/ToolSelect.tsx | 31 +++++++++++----- tests/InstallApp.test.tsx | 28 +++++++++------ tests/components/ToolSelect.test.tsx | 54 ++++++++++++++-------------- 3 files changed, 66 insertions(+), 47 deletions(-) diff --git a/src/components/ToolSelect.tsx b/src/components/ToolSelect.tsx index 2d46f69..3e7ea38 100644 --- a/src/components/ToolSelect.tsx +++ b/src/components/ToolSelect.tsx @@ -14,23 +14,36 @@ export type ToolSelectSentinel = | typeof CONTINUE_SENTINEL; export type ToolSelectValue = Tool | ToolSelectSentinel; -const TOOLS: { label: string; value: ToolSelectValue; locked?: boolean }[] = [ +const TOOLS: { + label: string; + value: ToolSelectValue; + locked?: boolean; + hidden?: boolean; +}[] = [ // CoDev Code is the flagship agent — always installed and configured, so its // row is shown pre-checked and can't be toggled off. Kept at index 0 so the // optional agents keep their positions. { label: "CoDev Code", value: "codev-code", locked: true }, { label: "Claude Code", value: "claude-code" }, - { label: "Codex", value: "codex" }, - { label: "OpenCode", value: "opencode" }, + // Codex and OpenCode are temporarily withheld from the selection UI. All the + // underlying configure/install/update logic still handles them end-to-end — + // they're only hidden from users until we're ready to surface them. Flip + // `hidden` off (or delete it) to bring the rows back. + { label: "Codex", value: "codex", hidden: true }, + { label: "OpenCode", value: "opencode", hidden: true }, { label: "Claude Code (extension)", value: CLAUDE_CODE_EXT_SENTINEL }, { label: "Continue (extension)", value: CONTINUE_SENTINEL }, ]; +// Rows actually rendered and navigable. Hidden tools are dropped from the UI +// only; everything downstream (Configure, restore, update) is untouched. +const VISIBLE_TOOLS = TOOLS.filter((t) => !t.hidden); + // Locked tools are emitted on every confirm regardless of the mutable // selection, and always lead the emitted list. -const LOCKED_VALUES: ToolSelectValue[] = TOOLS.filter((t) => t.locked).map( - (t) => t.value, -); +const LOCKED_VALUES: ToolSelectValue[] = VISIBLE_TOOLS.filter( + (t) => t.locked, +).map((t) => t.value); interface ToolSelectProps { onConfirm: (tools: ToolSelectValue[]) => void; @@ -51,9 +64,9 @@ export function ToolSelect({ if (key.upArrow) { setCursor((c) => Math.max(0, c - 1)); } else if (key.downArrow) { - setCursor((c) => Math.min(TOOLS.length - 1, c + 1)); + setCursor((c) => Math.min(VISIBLE_TOOLS.length - 1, c + 1)); } else if (input === " ") { - const tool = TOOLS[cursor]; + const tool = VISIBLE_TOOLS[cursor]; // Locked rows (CoDev Code) are always included and can't be toggled. if (!tool || tool.locked) return; setSelected((prev) => { @@ -79,7 +92,7 @@ export function ToolSelect({ return ( - {TOOLS.map((tool, i) => { + {VISIBLE_TOOLS.map((tool, i) => { const isSelected = tool.locked || selected.has(tool.value); const isCursor = !readOnly && cursor === i; return ( diff --git a/tests/InstallApp.test.tsx b/tests/InstallApp.test.tsx index c783064..5f575a4 100644 --- a/tests/InstallApp.test.tsx +++ b/tests/InstallApp.test.tsx @@ -504,7 +504,9 @@ describe("InstallApp fail-stop invariant", () => { expect(history).toContain("Happy coding"); }); - test("Codex selection routes to configureCodex and reaches done", async () => { + // Parked while Codex is hidden from ToolSelect (still fully wired downstream — + // just not user-selectable). Un-skip when the Codex row is surfaced again. + test.skip("Codex selection routes to configureCodex and reaches done", async () => { stubExecFile(() => ({ stdout: "ok" })); stubModels(); vi.spyOn(auth, "login").mockResolvedValue(fakeAuth()); @@ -1064,9 +1066,10 @@ describe("InstallApp fail-stop invariant", () => { const { stdin, frames } = render(); - // Pick the Claude Code (extension) row (5th, index 4). + // Pick the Claude Code (extension) row (3rd, index 2 — Codex/OpenCode + // are hidden, so it sits right below Claude Code). await waitForFrame(frames, "Select the AI agent(s) to install"); - for (let i = 0; i < 4; i++) { + for (let i = 0; i < 2; i++) { stdin.write("\x1B[B"); await new Promise((r) => setTimeout(r, 30)); } @@ -1096,8 +1099,9 @@ describe("InstallApp fail-stop invariant", () => { }); test("Claude Code CLI + extension share the backup kind: single configure call, both install tasks scheduled", async () => { - // Picks Claude Code CLI (2nd row) AND Claude Code (extension) (5th - // row), then VS Code in the merged sub-select. Asserts: + // Picks Claude Code CLI (2nd row) AND Claude Code (extension) (3rd + // row — Codex/OpenCode are hidden), then VS Code in the merged sub- + // select. Asserts: // - `configureClaudeCode` runs exactly once (shared BackupKind). // - Both the npm install task (@anthropic-ai/claude-code) and the // extension install task (anthropic.claude-code (VS Code)) appear. @@ -1126,15 +1130,14 @@ describe("InstallApp fail-stop invariant", () => { const { stdin, frames } = render(); await waitForFrame(frames, "Select the AI agent(s) to install"); - // Row 1 (Claude Code CLI) — toggle, then arrow down to row 4 and toggle. + // Row 1 (Claude Code CLI) — toggle, then arrow down to row 2 (Claude + // Code (extension), now directly below) and toggle. stdin.write("\x1B[B"); await new Promise((r) => setTimeout(r, 30)); stdin.write(" "); await new Promise((r) => setTimeout(r, 30)); - for (let i = 0; i < 3; i++) { - stdin.write("\x1B[B"); - await new Promise((r) => setTimeout(r, 30)); - } + stdin.write("\x1B[B"); + await new Promise((r) => setTimeout(r, 30)); stdin.write(" "); await new Promise((r) => setTimeout(r, 30)); stdin.write("\r"); @@ -1200,7 +1203,10 @@ describe("InstallApp fail-stop invariant", () => { }); }); - test("partial install failure: survivor advances to Configure, failed tool is dropped", async () => { + // Parked while Codex is hidden from ToolSelect: this exercises the + // survivor-advances fail-stop via a Codex selection that's no longer + // user-reachable. Un-skip when the Codex row is surfaced again. + test.skip("partial install failure: survivor advances to Configure, failed tool is dropped", async () => { // User selects both Claude Code and Codex. The codex npm install // hard-fails ("disk full"); the claude-code one succeeds. Pre-change // behavior was to park at install-failed and force the user to Ctrl-C. diff --git a/tests/components/ToolSelect.test.tsx b/tests/components/ToolSelect.test.tsx index ed4fe4e..afac5c9 100644 --- a/tests/components/ToolSelect.test.tsx +++ b/tests/components/ToolSelect.test.tsx @@ -7,19 +7,29 @@ afterEach(() => { }); describe("ToolSelect", () => { - test("renders all tool options", () => { + test("renders the visible tool options", () => { const onConfirm = vi.fn(); const { lastFrame } = render(); const output = lastFrame() ?? ""; expect(output).toContain("CoDev Code"); expect(output).toContain("Claude Code"); - expect(output).toContain("OpenCode"); - expect(output).toContain("Codex"); expect(output).toContain("Claude Code (extension)"); expect(output).toContain("Continue (extension)"); }); + test("hides Codex and OpenCode from the selection UI", () => { + // Both are temporarily withheld from users — still fully wired downstream, + // just not rendered or selectable here. Remove the `hidden` flag in + // ToolSelect's TOOLS array to bring them back. + const onConfirm = vi.fn(); + const { lastFrame } = render(); + + const output = lastFrame() ?? ""; + expect(output).not.toContain("Codex"); + expect(output).not.toContain("OpenCode"); + }); + test("emits the `claude-code-ext` sentinel when the Claude Code (extension) row is picked", async () => { // The extension rows are editor-agnostic; the merged editor sub- // select runs next. ToolSelect emits a sentinel that InstallApp @@ -28,8 +38,10 @@ describe("ToolSelect", () => { const onConfirm = vi.fn(); const { stdin } = render(); - // Four down-arrows to reach the 5th (Claude Code (extension)) row. - for (let i = 0; i < 4; i++) { + // Two down-arrows to reach the 3rd (Claude Code (extension)) row — + // Codex and OpenCode are hidden, so the extension rows sit right below + // Claude Code. + for (let i = 0; i < 2; i++) { stdin.write("\x1B[B"); await new Promise((r) => setTimeout(r, 50)); } @@ -45,8 +57,10 @@ describe("ToolSelect", () => { const onConfirm = vi.fn(); const { stdin } = render(); - // Five down-arrows to reach the 6th (Continue (extension)) row. - for (let i = 0; i < 5; i++) { + // Three down-arrows to reach the 4th (Continue (extension)) row — + // Codex and OpenCode are hidden, so it sits directly below the Claude + // Code (extension) row. + for (let i = 0; i < 3; i++) { stdin.write("\x1B[B"); await new Promise((r) => setTimeout(r, 50)); } @@ -63,8 +77,8 @@ describe("ToolSelect", () => { const { lastFrame } = render(); const output = lastFrame() ?? ""; - // Exactly one filled box — the always-on CoDev Code row — and the five - // optional agents render unchecked. + // Exactly one filled box — the always-on CoDev Code row — and the + // remaining optional agents render unchecked. expect((output.match(/■/g) ?? []).length).toBe(1); expect(output).toContain("□"); expect(output).toContain("(always installed)"); @@ -139,7 +153,9 @@ describe("ToolSelect", () => { const onConfirm = vi.fn(); const { stdin } = render(); - // Down to Claude Code (row 1), select, down to Codex (row 2), select. + // Down to Claude Code (row 1), select, down to Claude Code (extension) + // (row 2), select. Codex/OpenCode are hidden, so the extension row is + // the next selectable option below Claude Code. stdin.write("\x1B[B"); await new Promise((r) => setTimeout(r, 50)); stdin.write(" "); @@ -156,26 +172,10 @@ describe("ToolSelect", () => { expect(onConfirm).toHaveBeenCalledWith([ "codev-code", "claude-code", - "codex", + "claude-code-ext", ]); }); - test("can select Codex by moving cursor down twice", async () => { - const onConfirm = vi.fn(); - const { stdin } = render(); - - stdin.write("\x1B[B"); - await new Promise((r) => setTimeout(r, 50)); - stdin.write("\x1B[B"); - await new Promise((r) => setTimeout(r, 50)); - stdin.write(" "); - await new Promise((r) => setTimeout(r, 50)); - stdin.write("\r"); - await new Promise((r) => setTimeout(r, 50)); - - expect(onConfirm).toHaveBeenCalledWith(["codev-code", "codex"]); - }); - test("can deselect a tool", async () => { const onConfirm = vi.fn(); const { lastFrame, stdin } = render();