mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-09-30 12:39:42 +02:00
feat(settings): show the Codex CLI tab only where codex is installed
Both settings on the App Settings "Codex CLI" tab (bypass approvals, animated status effects) are handed to `codex` at launch, so on an instance where the binary does not resolve the tab offers choices nothing can act on. Gate it on availability instead. renderIndexHtml injects window.__codemanCodexAvailable, mirroring the existing gesture-availability flag, and settings-ui.js hides the tab button when it is absent. Injected rather than fetched on modal open so the tab cannot flicker in and back out; isCodexAvailable() memoizes its PATH probe, so the per-render cost is nil. Installing codex later needs a restart, exactly like the /api/codex/status route that already backs the Run menu. Solo popups skip the probe since they have no settings modal. Only the tab BUTTON is toggled. The panel already carries .modal-tab-content.hidden unless it is the selected tab and openAppSettings() always reopens on Display, so an unreachable button keeps the panel unreachable. The inputs stay in the DOM and are still populated and read back on save, so a user without codex cannot silently wipe the codex preferences of an instance that has it. Animations stay off by default for new local Codex sessions. Verified in a browser on this host, which has no codex: the flag is absent, the Codex tab is hidden while the other tabs are unaffected, and saving App Settings with the tab hidden leaves codexAnimationsEnabled/codexDangerouslyBypassApprovals untouched. With the flag forced on, the tab appears, its panel opens, and toggling the visible slider persists. The openAppSettings coupling test was checked to fail when the call is removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -293,6 +293,58 @@ describe('Codex quick start settings', () => {
|
||||
expect(codexTab?.[1]).not.toContain('appSettingsCodexRenderMode');
|
||||
});
|
||||
|
||||
describe('Codex CLI tab visibility', () => {
|
||||
// Both settings on the tab are handed to `codex` at launch, so on an instance
|
||||
// where the binary does not resolve the tab is a promise nothing can keep.
|
||||
// renderIndexHtml injects window.__codemanCodexAvailable; this pins the client
|
||||
// half. Coupled test: it drives the REAL settings-ui.js against a stub button,
|
||||
// so deleting the call in openAppSettings() is what it is meant to catch.
|
||||
function loadSettingsUi(codexAvailable: boolean | undefined) {
|
||||
const codexTabBtn = { dataset: { tab: 'settings-codex' }, style: { display: 'PRISTINE' } };
|
||||
const CodemanApp = function CodemanApp(this: any) {};
|
||||
const context: any = vm.createContext({
|
||||
CodemanApp,
|
||||
MobileDetection: { getDeviceType: () => 'desktop', isTouchDevice: () => false, isHandheldDevice: () => false },
|
||||
localStorage: { getItem: () => null, setItem: () => {} },
|
||||
document: {
|
||||
getElementById: () => null,
|
||||
querySelector: (sel: string) => (sel.includes('[data-tab="settings-codex"]') ? codexTabBtn : null),
|
||||
},
|
||||
console,
|
||||
});
|
||||
context.window = context;
|
||||
if (codexAvailable !== undefined) context.__codemanCodexAvailable = codexAvailable;
|
||||
const settingsUi = readFileSync(resolve(import.meta.dirname, '../src/web/public/settings-ui.js'), 'utf8');
|
||||
vm.runInContext(settingsUi, context, { filename: 'settings-ui.js' });
|
||||
return { app: new (CodemanApp as any)(), codexTabBtn };
|
||||
}
|
||||
|
||||
it('hides the Codex tab when the codex binary is not available', () => {
|
||||
const { app, codexTabBtn } = loadSettingsUi(false);
|
||||
app._applyCodexSettingsVisibility();
|
||||
expect(codexTabBtn.style.display).toBe('none');
|
||||
});
|
||||
|
||||
it('hides the Codex tab when the availability flag was never injected', () => {
|
||||
const { app, codexTabBtn } = loadSettingsUi(undefined);
|
||||
app._applyCodexSettingsVisibility();
|
||||
expect(codexTabBtn.style.display).toBe('none');
|
||||
});
|
||||
|
||||
it('shows the Codex tab when codex is available', () => {
|
||||
const { app, codexTabBtn } = loadSettingsUi(true);
|
||||
app._applyCodexSettingsVisibility();
|
||||
expect(codexTabBtn.style.display).toBe('');
|
||||
});
|
||||
|
||||
it('applies the gating from openAppSettings, not just in isolation', () => {
|
||||
const src = readFileSync(resolve(import.meta.dirname, '../src/web/public/settings-ui.js'), 'utf8');
|
||||
const open = src.slice(src.indexOf('\n openAppSettings() {'));
|
||||
const body = open.slice(0, open.indexOf('\n },'));
|
||||
expect(body).toContain('_applyCodexSettingsVisibility()');
|
||||
});
|
||||
});
|
||||
|
||||
it('passes global Codex settings into quick-start config for new sessions', async () => {
|
||||
const elements: Record<string, any> = {
|
||||
quickStartCase: { value: 'codex-case' },
|
||||
|
||||
Reference in New Issue
Block a user