mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
Merge #550: keep Respawn and Ralph visible in Session Options for Claude sessions
This commit is contained in:
@@ -151,6 +151,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.
|
||||||
@@ -2477,7 +2484,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
|
||||||
|
|||||||
@@ -3226,12 +3226,18 @@ Object.assign(CodemanApp.prototype, {
|
|||||||
/** Keep the launch surfaces in sync with Settings mutations without a reload. */
|
/** Keep the launch surfaces in sync with Settings mutations without a reload. */
|
||||||
_syncCliLaunchCatalog() {
|
_syncCliLaunchCatalog() {
|
||||||
if (!Array.isArray(this._cliList) || this._cliList.length === 0) return;
|
if (!Array.isArray(this._cliList) || this._cliList.length === 0) return;
|
||||||
|
// /api/clis rows carry no capabilities, so keep the served catalog's `external`
|
||||||
|
// (isExternalCliSession() reads it). A new custom CLI has none and falls back to `kind`.
|
||||||
|
const previous = new Map(
|
||||||
|
(Array.isArray(window.__codemanCliCatalog) ? window.__codemanCliCatalog : []).map((cli) => [cli.id, cli])
|
||||||
|
);
|
||||||
window.__codemanCliCatalog = this._cliList.map((cli) => ({
|
window.__codemanCliCatalog = this._cliList.map((cli) => ({
|
||||||
id: cli.id,
|
id: cli.id,
|
||||||
label: cli.label,
|
label: cli.label,
|
||||||
shortBadge: cli.shortBadge,
|
shortBadge: cli.shortBadge,
|
||||||
order: cli.order,
|
order: cli.order,
|
||||||
kind: cli.kind,
|
kind: cli.kind,
|
||||||
|
external: previous.get(cli.id)?.external,
|
||||||
enabled: cli.enabled,
|
enabled: cli.enabled,
|
||||||
available: cli.kind === 'shell' || (cli.enabled && cli.installed),
|
available: cli.kind === 'shell' || (cli.enabled && cli.installed),
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -312,7 +312,9 @@ export function registerCliRegistryRoutes(app: FastifyInstance): void {
|
|||||||
// admin/settings surface; every SPAWN-time caller elsewhere uses
|
// admin/settings surface; every SPAWN-time caller elsewhere uses
|
||||||
// enabledClis() instead). Deliberately excludes launch/env/capabilities/
|
// enabledClis() instead). Deliberately excludes launch/env/capabilities/
|
||||||
// overlays/discovery — the same rule every other catalogue-export surface in
|
// overlays/discovery — the same rule every other catalogue-export surface in
|
||||||
// this codebase follows.
|
// this codebase follows, with one deliberate exception: the page catalog
|
||||||
|
// (`window.__codemanCliCatalog`, server.ts) carries `capabilities.external`,
|
||||||
|
// which Session Options reads to keep Claude's Respawn and Ralph tabs.
|
||||||
//
|
//
|
||||||
// NOT gated on cliManagementEnabled: reading the list is cheap and is not
|
// NOT gated on cliManagementEnabled: reading the list is cheap and is not
|
||||||
// the risky part. The Settings UI section simply never fetches this while
|
// the risky part. The Settings UI section simply never fetches this while
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 = {
|
||||||
@@ -651,6 +659,34 @@ 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('keeps the served external flag when App Settings resyncs the catalog from /api/clis', () => {
|
||||||
|
const catalog = CATALOG.map((cli) => ({ ...cli, external: cli.id !== 'claude' && cli.id !== 'shell' }));
|
||||||
|
const { app, context } = loadUi(undefined, catalog);
|
||||||
|
// /api/clis rows: no capabilities, so no `external`.
|
||||||
|
app._cliList = CATALOG.map((cli) => ({ ...cli, installed: true }));
|
||||||
|
app.runMode = 'claude';
|
||||||
|
app._syncCliLaunchCatalog();
|
||||||
|
expect(context.isExternalCliSession('claude')).toBe(false);
|
||||||
|
expect(context.isExternalCliSession('codex')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('gates Session Options on isExternalCliSession, not the launch-path check', () => {
|
||||||
|
const src = readFileSync(resolve(import.meta.dirname, '../src/web/public/session-ui.js'), 'utf8');
|
||||||
|
const open = src.slice(src.indexOf('\n openSessionOptions('));
|
||||||
|
const body = open.slice(0, open.indexOf('\n },'));
|
||||||
|
expect(body).toContain('isExternalCliSession(session.mode)');
|
||||||
|
expect(body).not.toContain('isExternalCliRunMode(');
|
||||||
|
});
|
||||||
|
|
||||||
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.
|
||||||
|
|||||||
Reference in New Issue
Block a user