From 45492a30137925121b450a6c60a7aa75fcd5b1f2 Mon Sep 17 00:00:00 2001 From: JD Date: Wed, 7 Oct 2026 22:08:28 -0400 Subject: [PATCH] fix(session-options): keep Respawn and Ralph visible for Claude sessions Since the CLI registry gave claude kind 'agent' (#476), isExternalCliRunMode() reads claude as an external CLI, so Session Options opened every Claude session on Summary and hid the Respawn and Ralph tabs and every Claude-only control (auto-resume, the respawn loop). The browser catalog now carries the registry's capabilities.external, the flag the server's isExternalCliMode() already reads, and Session Options asks that instead. run() keeps isExternalCliRunMode(): choosing a launch path is a different question, and custom agents rely on it. --- src/web/public/session-ui.js | 9 ++++++++- src/web/server.ts | 1 + test/render-index-html.test.ts | 11 +++++++++-- test/run-mode-ui.test.ts | 19 ++++++++++++++++++- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/web/public/session-ui.js b/src/web/public/session-ui.js index 9b2bf1b4..b13497a7 100644 --- a/src/web/public/session-ui.js +++ b/src/web/public/session-ui.js @@ -140,6 +140,13 @@ function isExternalCliRunMode(mode) { return EXTERNAL_CLI_MODES.has(mode) || registryCliById(mode)?.kind === 'agent'; } +// Does this session lack the Claude-only features (Respawn, Ralph)? The registry's +// `capabilities.external`, the flag the server's isExternalCliMode() reads. Not +// isExternalCliRunMode(): that picks a launch path, and claude is `kind: 'agent'` too. +function isExternalCliSession(mode) { + return registryCliById(mode)?.external ?? isExternalCliRunMode(mode); +} + Object.assign(CodemanApp.prototype, { /** * Build envOverrides payload from case + global settings. @@ -2430,7 +2437,7 @@ Object.assign(CodemanApp.prototype, { if (detachToggle) detachToggle.checked = this.hasTabDetachOverride(sessionId); // Reset to an appropriate tab — Summary for external CLIs (Respawn/Ralph are Claude-only) - const isAltMode = isExternalCliRunMode(session.mode); + const isAltMode = isExternalCliSession(session.mode); this.switchOptionsTab(isAltMode ? 'summary' : 'respawn'); // Update respawn status display and buttons diff --git a/src/web/server.ts b/src/web/server.ts index 4442e544..2f00ae58 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -1713,6 +1713,7 @@ export class WebServer extends EventEmitter { shortBadge: entry.shortBadge, order: entry.order, kind: entry.kind, + external: entry.capabilities.external, enabled, available: enabled && installed, }; diff --git a/test/render-index-html.test.ts b/test/render-index-html.test.ts index 7f541443..9d3427cc 100644 --- a/test/render-index-html.test.ts +++ b/test/render-index-html.test.ts @@ -248,16 +248,23 @@ describe('WebServer.renderIndexHtml', () => { shortBadge: string; order: number; kind: string; + external: boolean; enabled: boolean; available: boolean; }>; expect(catalog.map((entry) => entry.id)).toEqual(STOCK_CLIS.map((entry) => entry.id)); - expect(catalog.find((entry) => entry.id === 'codex')).toMatchObject({ label: 'Codex', kind: 'agent' }); + expect(catalog.find((entry) => entry.id === 'codex')).toMatchObject({ + label: 'Codex', + kind: 'agent', + external: true, + }); + // claude is `kind: 'agent'` too; `external` is what keeps its Respawn/Ralph options visible. + expect(catalog.find((entry) => entry.id === 'claude')).toMatchObject({ kind: 'agent', external: false }); expect(catalog.find((entry) => entry.id === 'shell')).toMatchObject({ enabled: true, available: true }); expect( catalog.every((entry) => Object.keys(entry).every((key) => - ['id', 'label', 'shortBadge', 'order', 'kind', 'enabled', 'available'].includes(key) + ['id', 'label', 'shortBadge', 'order', 'kind', 'external', 'enabled', 'available'].includes(key) ) ) ).toBe(true); diff --git a/test/run-mode-ui.test.ts b/test/run-mode-ui.test.ts index b290cf8f..d237b129 100644 --- a/test/run-mode-ui.test.ts +++ b/test/run-mode-ui.test.ts @@ -453,7 +453,15 @@ describe('Codex quick start settings', () => { const src = readFileSync(resolve(import.meta.dirname, `../src/web/public/${file}`), 'utf8'); vm.runInContext(src, context, { filename: file }); } - return { app: new (CodemanApp as any)(), welcomeCliActions, tunnelBtn, runModeCliOptions, modeBtns, menu }; + return { + app: new (CodemanApp as any)(), + welcomeCliActions, + tunnelBtn, + runModeCliOptions, + modeBtns, + menu, + context, + }; } const ALL_OFF = { @@ -539,6 +547,15 @@ describe('Codex quick start settings', () => { expect(src).not.toContain('data-mode="codex"'); }); + it('keeps Claude-only session options for claude even though it is a registry agent', () => { + const catalog = CATALOG.map((cli) => ({ ...cli, external: cli.id !== 'claude' && cli.id !== 'shell' })); + const { context } = loadUi(undefined, catalog); + expect(context.isExternalCliSession('claude')).toBe(false); + expect(context.isExternalCliSession('shell')).toBe(false); + expect(context.isExternalCliSession('codex')).toBe(true); + expect(context.isExternalCliSession('custom-agent')).toBe(true); + }); + it('shows everything when the flags were never injected', () => { // A cached page from a build without the injection, or a solo popup. Hiding // every run button on a doubt would leave a working install nothing to click.