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.
This commit is contained in:
JD
2026-10-08 00:15:50 -04:00
parent ac94f339ac
commit 45492a3013
4 changed files with 36 additions and 4 deletions
+8 -1
View File
@@ -140,6 +140,13 @@ function isExternalCliRunMode(mode) {
return EXTERNAL_CLI_MODES.has(mode) || registryCliById(mode)?.kind === 'agent'; 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, { Object.assign(CodemanApp.prototype, {
/** /**
* Build envOverrides payload from case + global settings. * Build envOverrides payload from case + global settings.
@@ -2430,7 +2437,7 @@ Object.assign(CodemanApp.prototype, {
if (detachToggle) detachToggle.checked = this.hasTabDetachOverride(sessionId); if (detachToggle) detachToggle.checked = this.hasTabDetachOverride(sessionId);
// Reset to an appropriate tab — Summary for external CLIs (Respawn/Ralph are Claude-only) // 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'); this.switchOptionsTab(isAltMode ? 'summary' : 'respawn');
// Update respawn status display and buttons // Update respawn status display and buttons
+1
View File
@@ -1713,6 +1713,7 @@ export class WebServer extends EventEmitter {
shortBadge: entry.shortBadge, shortBadge: entry.shortBadge,
order: entry.order, order: entry.order,
kind: entry.kind, kind: entry.kind,
external: entry.capabilities.external,
enabled, enabled,
available: enabled && installed, available: enabled && installed,
}; };
+9 -2
View File
@@ -248,16 +248,23 @@ describe('WebServer.renderIndexHtml', () => {
shortBadge: string; shortBadge: string;
order: number; order: number;
kind: string; kind: string;
external: boolean;
enabled: boolean; enabled: boolean;
available: boolean; available: boolean;
}>; }>;
expect(catalog.map((entry) => entry.id)).toEqual(STOCK_CLIS.map((entry) => entry.id)); 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.find((entry) => entry.id === 'shell')).toMatchObject({ enabled: true, available: true });
expect( expect(
catalog.every((entry) => catalog.every((entry) =>
Object.keys(entry).every((key) => 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); ).toBe(true);
+18 -1
View File
@@ -453,7 +453,15 @@ describe('Codex quick start settings', () => {
const src = readFileSync(resolve(import.meta.dirname, `../src/web/public/${file}`), 'utf8'); const src = readFileSync(resolve(import.meta.dirname, `../src/web/public/${file}`), 'utf8');
vm.runInContext(src, context, { filename: file }); 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 = { const ALL_OFF = {
@@ -539,6 +547,15 @@ describe('Codex quick start settings', () => {
expect(src).not.toContain('data-mode="codex"'); 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', () => { it('shows everything when the flags were never injected', () => {
// A cached page from a build without the injection, or a solo popup. Hiding // 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. // every run button on a doubt would leave a working install nothing to click.