synabun 2.0.5 → 2.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +2 -1
- package/THIRD-PARTY-LICENSES.md +13 -0
- package/docs/whatsapp.md +151 -16
- package/hooks/claude-code/shared.mjs +6 -3
- package/lib/child-path.js +303 -0
- package/lib/claude-hooks.js +2 -1
- package/lib/data-home-migration.js +2 -0
- package/lib/desktop-app.js +265 -0
- package/lib/external-tools.js +7 -3
- package/lib/log-redact.js +102 -0
- package/lib/packaged-runtime.js +138 -11
- package/mcp-server/dist/http.d.ts +3 -1
- package/mcp-server/dist/http.d.ts.map +1 -1
- package/mcp-server/dist/http.js +15 -4
- package/mcp-server/dist/http.js.map +1 -1
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +9 -6
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/budget-judgments.js +153 -0
- package/mcp-server/dist/services/budget-judgments.js.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +41 -4
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +11 -0
- package/mcp-server/dist/services/identity.d.ts.map +1 -1
- package/mcp-server/dist/services/identity.js +24 -0
- package/mcp-server/dist/services/identity.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +13 -0
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +34 -2
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
- package/mcp-server/dist/services/typesafe-config.js +4 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +10 -7
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +34 -13
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
- package/mcp-server/dist/tui.js +2 -1
- package/mcp-server/dist/tui.js.map +1 -1
- package/mcp-server/dist/version.d.ts +5 -0
- package/mcp-server/dist/version.d.ts.map +1 -0
- package/mcp-server/dist/version.js +21 -0
- package/mcp-server/dist/version.js.map +1 -0
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -1
- package/mcp-server/run.mjs +15 -9
- package/mcp-server/src/http.ts +18 -3
- package/mcp-server/src/index.ts +9 -6
- package/mcp-server/src/services/budget-judgments.ts +183 -0
- package/mcp-server/src/services/codex-config-heal.ts +35 -4
- package/mcp-server/src/services/identity.ts +27 -0
- package/mcp-server/src/services/neural-interface.ts +34 -2
- package/mcp-server/src/services/typesafe-config.ts +4 -1
- package/mcp-server/src/tools/agents.ts +33 -14
- package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
- package/mcp-server/src/tui.ts +2 -1
- package/mcp-server/src/version.ts +22 -0
- package/neural-interface/i18n/en.json +627 -57
- package/neural-interface/i18n/pt-BR.json +626 -56
- package/neural-interface/lib/account-usage.js +163 -0
- package/neural-interface/lib/assistant-api.js +255 -52
- package/neural-interface/lib/assistant-brains/claude.js +15 -2
- package/neural-interface/lib/assistant-brains/codex.js +3 -0
- package/neural-interface/lib/assistant-brains/opencode.js +17 -4
- package/neural-interface/lib/assistant-budget-planner.js +390 -0
- package/neural-interface/lib/assistant-budget-split.js +408 -0
- package/neural-interface/lib/assistant-budget.js +94 -95
- package/neural-interface/lib/assistant-config.js +62 -14
- package/neural-interface/lib/assistant-dispatch.js +432 -125
- package/neural-interface/lib/assistant-jev.js +26 -3
- package/neural-interface/lib/assistant-persona.js +68 -11
- package/neural-interface/lib/assistant-plan-permissions.js +8 -1
- package/neural-interface/lib/assistant-route-accounts.js +14 -0
- package/neural-interface/lib/assistant-route-gate.js +19 -3
- package/neural-interface/lib/assistant-router.js +373 -42
- package/neural-interface/lib/assistant-runtime.js +307 -47
- package/neural-interface/lib/assistant-user-writer.js +4 -2
- package/neural-interface/lib/augmented-path.js +317 -53
- package/neural-interface/lib/claude-accounts.js +342 -31
- package/neural-interface/lib/claude-agent-bridge.js +90 -3
- package/neural-interface/lib/claude-executable.js +6 -1
- package/neural-interface/lib/claude-model-catalog.js +3 -3
- package/neural-interface/lib/claude-panel-auth.js +221 -0
- package/neural-interface/lib/claude-profile-api-key.js +62 -0
- package/neural-interface/lib/cli-install-api.js +137 -0
- package/neural-interface/lib/cli-install.js +797 -0
- package/neural-interface/lib/cli-path.js +130 -0
- package/neural-interface/lib/cli-resolver.js +744 -0
- package/neural-interface/lib/cli-update-planner.js +143 -7
- package/neural-interface/lib/codex-account-auth.js +178 -0
- package/neural-interface/lib/codex-accounts.js +489 -0
- package/neural-interface/lib/codex-capabilities.js +2 -1
- package/neural-interface/lib/codex-compat-rpc.js +8 -2
- package/neural-interface/lib/codex-mcp-approvals.js +5 -1
- package/neural-interface/lib/codex-panel-auth.js +62 -0
- package/neural-interface/lib/codex-session-settings.js +1 -1
- package/neural-interface/lib/codex-settings-schema.js +120 -0
- package/neural-interface/lib/desktop/config.js +3 -0
- package/neural-interface/lib/git-bash/bzip2.js +339 -0
- package/neural-interface/lib/git-bash/claude-settings.js +73 -0
- package/neural-interface/lib/git-bash/extract-worker.js +34 -0
- package/neural-interface/lib/git-bash/install.js +529 -0
- package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
- package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
- package/neural-interface/lib/git-bash/manager.js +268 -0
- package/neural-interface/lib/git-bash/pins.js +206 -0
- package/neural-interface/lib/git-bash/pins.json +34 -0
- package/neural-interface/lib/git-bash/resolver.js +194 -0
- package/neural-interface/lib/git-read.js +34 -2
- package/neural-interface/lib/install-details.js +145 -0
- package/neural-interface/lib/mcp-installer.js +14 -2
- package/neural-interface/lib/native-loop-providers.js +16 -3
- package/neural-interface/lib/native-loop-runtime.js +9 -4
- package/neural-interface/lib/os-path.js +319 -0
- package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
- package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
- package/neural-interface/lib/pty-host/core.js +2 -1
- package/neural-interface/lib/pty-host/manager.js +27 -2
- package/neural-interface/lib/remote-policy.js +20 -11
- package/neural-interface/lib/session-title-generator.js +4 -3
- package/neural-interface/lib/system-about.js +340 -0
- package/neural-interface/lib/system-activity.js +66 -0
- package/neural-interface/lib/terminal-env.js +11 -3
- package/neural-interface/lib/terminal-path.js +55 -0
- package/neural-interface/lib/virtual-socket.js +12 -1
- package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
- package/neural-interface/lib/whatsapp/bridge.js +78 -34
- package/neural-interface/lib/whatsapp/commands.js +13 -14
- package/neural-interface/lib/whatsapp/config.js +134 -20
- package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
- package/neural-interface/lib/whatsapp/host-core.js +168 -35
- package/neural-interface/lib/whatsapp/inbound.js +5 -3
- package/neural-interface/lib/whatsapp/local-file.js +524 -0
- package/neural-interface/lib/whatsapp/manager.js +92 -32
- package/neural-interface/lib/whatsapp/paths.js +11 -1
- package/neural-interface/lib/whatsapp/protocol.js +65 -5
- package/neural-interface/lib/whatsapp/redact.js +63 -0
- package/neural-interface/lib/whatsapp/service.js +343 -91
- package/neural-interface/lib/whatsapp/transcription.js +463 -0
- package/neural-interface/package-lock.json +2 -2
- package/neural-interface/package.json +1 -1
- package/neural-interface/public/index.html +2 -0
- package/neural-interface/public/index2d.html +2 -0
- package/neural-interface/public/offline.html +16 -1
- package/neural-interface/public/onboarding.html +96 -2
- package/neural-interface/public/shared/account-i18n.js +7 -0
- package/neural-interface/public/shared/account-menu.js +539 -0
- package/neural-interface/public/shared/account-usage.js +19 -0
- package/neural-interface/public/shared/account-view.js +335 -0
- package/neural-interface/public/shared/agent-runtime-options.js +28 -8
- package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
- package/neural-interface/public/shared/assistant/asst-api.js +32 -2
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +124 -25
- package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
- package/neural-interface/public/shared/assistant/asst-mascot.js +126 -7
- package/neural-interface/public/shared/assistant/asst-menu.js +15 -3
- package/neural-interface/public/shared/assistant/asst-panel.js +48 -14
- package/neural-interface/public/shared/assistant/asst-render.js +38 -7
- package/neural-interface/public/shared/assistant/asst-route.js +72 -14
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +45 -6
- package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
- package/neural-interface/public/shared/assistant/asst-state.js +1 -1
- package/neural-interface/public/shared/assistant/asst-styles.js +99 -17
- package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
- package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
- package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +48 -107
- package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +128 -107
- package/neural-interface/public/shared/cdx/cdx-tabs.js +261 -664
- package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
- package/neural-interface/public/shared/claude-account-view.js +3 -0
- package/neural-interface/public/shared/cli-install-view.js +392 -0
- package/neural-interface/public/shared/cli-status.js +282 -22
- package/neural-interface/public/shared/cp/cp-events.js +2 -1
- package/neural-interface/public/shared/cp/cp-styles.js +46 -0
- package/neural-interface/public/shared/desktop-bridge.js +39 -0
- package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
- package/neural-interface/public/shared/desktop-titlebar.js +153 -0
- package/neural-interface/public/shared/desktop-update-view.js +462 -0
- package/neural-interface/public/shared/editor/editor-panel.js +1 -1
- package/neural-interface/public/shared/explorer/fx-main.js +1 -1
- package/neural-interface/public/shared/git-bash-view.js +145 -0
- package/neural-interface/public/shared/html-shell.js +1 -0
- package/neural-interface/public/shared/i18n.js +19 -1
- package/neural-interface/public/shared/install-details-view.js +86 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +32 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +17 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +118 -0
- package/neural-interface/public/shared/pwa-install-page.js +3 -1
- package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
- package/neural-interface/public/shared/settings/settings-about.js +254 -0
- package/neural-interface/public/shared/settings/settings-ia.js +15 -0
- package/neural-interface/public/shared/showcase-privacy.js +489 -0
- package/neural-interface/public/shared/storage.js +30 -3
- package/neural-interface/public/shared/styles.css +278 -31
- package/neural-interface/public/shared/synabun-mascot.js +48 -2
- package/neural-interface/public/shared/ui-claude-panel.js +212 -213
- package/neural-interface/public/shared/ui-cli-banner.js +355 -0
- package/neural-interface/public/shared/ui-git-bash.js +124 -0
- package/neural-interface/public/shared/ui-judgments.js +10 -1
- package/neural-interface/public/shared/ui-loading.js +30 -6
- package/neural-interface/public/shared/ui-menubar.js +13 -3
- package/neural-interface/public/shared/ui-navbar.js +3 -1
- package/neural-interface/public/shared/ui-provider-auth.js +104 -0
- package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
- package/neural-interface/public/shared/ui-sessions.js +9 -3
- package/neural-interface/public/shared/ui-settings.js +55 -3
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
- package/neural-interface/public/shared/ui-sync.js +6 -0
- package/neural-interface/public/shared/ui-terminal.js +17 -0
- package/neural-interface/public/shared/ui-tool-updates.js +5 -0
- package/neural-interface/public/shared/ui-update.js +594 -5
- package/neural-interface/public/shared/ui-whatsapp.js +21 -6
- package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
- package/neural-interface/public/shared/worker-end.js +2 -0
- package/neural-interface/public/sw.js +1 -1
- package/neural-interface/public/variant/2d/main.js +10 -1
- package/neural-interface/public/variant/3d/main.js +10 -1
- package/neural-interface/server.js +1219 -608
- package/package.json +4 -1
- package/setup.js +9 -8
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { capabilityAvailability, flattenNativeCatalog, reviewTarget } from './cdx-commands.js';
|
|
2
|
+
import { t } from '../i18n.js';
|
|
2
3
|
import { knownCodexCapabilities } from './cdx-capabilities.js';
|
|
3
4
|
import { codexReadableValue, parseCodexAttachmentPayload, codexGatewayState, codexVerificationState, codexHttpsHandoff } from './cdx-protocol.js';
|
|
4
5
|
|
|
@@ -333,23 +334,27 @@ export function updateCodexGateway(tab, value) {
|
|
|
333
334
|
accountSecurityViews.get(tab)?.();
|
|
334
335
|
}
|
|
335
336
|
|
|
336
|
-
export async function mountCodexAccountSecurity({ host, request, tab, capabilities }) {
|
|
337
|
+
export async function mountCodexAccountSecurity({ host, experimentalHost = host, request, tab, capabilities }) {
|
|
338
|
+
const tr = (key, params) => t(`codexSettings.${key}`, params);
|
|
337
339
|
const view = { error: message => { error.textContent = message; } };
|
|
338
340
|
const error = element('div', 'cxp-settings-hint'); error.setAttribute('role', 'alert');
|
|
339
341
|
const gateway = element('div', 'cxp-settings-section');
|
|
340
342
|
const verification = element('div', 'cxp-settings-section');
|
|
341
|
-
host.append(gateway,
|
|
343
|
+
host.append(gateway, error);
|
|
344
|
+
experimentalHost.append(verification);
|
|
342
345
|
const available = name => capabilityAvailability(knownCodexCapabilities(tab) || capabilities, name);
|
|
343
346
|
const renderGateway = () => {
|
|
344
347
|
if (!host.isConnected || tab.closed) { accountSecurityViews.delete(tab); return; }
|
|
345
348
|
gateway.replaceChildren();
|
|
346
349
|
if (!available('gateway_oauth_read').supported) return;
|
|
347
350
|
const value = tab.gatewayOAuth || {};
|
|
351
|
+
gateway.hidden = !value.required;
|
|
352
|
+
if (!value.required) return;
|
|
348
353
|
const state = codexGatewayState(value);
|
|
349
|
-
gateway.append(element('div', 'cxp-settings-section-title', '
|
|
350
|
-
gateway.append(element('div', 'cxp-settings-hint', `${value.providerName || value.providerId || '
|
|
354
|
+
gateway.append(element('div', 'cxp-settings-section-title', tr('gateway')));
|
|
355
|
+
gateway.append(element('div', 'cxp-settings-hint', `${value.providerName || value.providerId || tr('provider')} · ${tr(`gatewayStates.${value.status || 'notReady'}`)}`));
|
|
351
356
|
const action = state.pending ? 'gateway_oauth_cancel' : 'gateway_oauth_login';
|
|
352
|
-
gateway.append(button(state.pending ? '
|
|
357
|
+
gateway.append(button(tr(state.pending ? 'cancel' : 'signIn'), async () => {
|
|
353
358
|
if (!available(action).supported) throw new Error(available(action).reason);
|
|
354
359
|
await request(action, {});
|
|
355
360
|
// Notification is authoritative; do not overwrite a completed login with a stale optimistic state.
|
|
@@ -360,10 +365,10 @@ export async function mountCodexAccountSecurity({ host, request, tab, capabiliti
|
|
|
360
365
|
}, view, available(action)));
|
|
361
366
|
if (!available(action).supported) gateway.append(element('div', 'cxp-settings-hint', available(action).reason));
|
|
362
367
|
const handoff = codexHttpsHandoff(value.authUrl);
|
|
363
|
-
if (value.authUrl && !handoff) gateway.append(element('div', 'cxp-settings-hint', '
|
|
368
|
+
if (value.authUrl && !handoff) gateway.append(element('div', 'cxp-settings-hint', tr('httpsRequired')));
|
|
364
369
|
if (handoff && state.pending) {
|
|
365
|
-
gateway.append(element('div', 'cxp-settings-hint',
|
|
366
|
-
gateway.append(button(
|
|
370
|
+
gateway.append(element('div', 'cxp-settings-hint', tr('continueAt', { host: handoff.host })));
|
|
371
|
+
gateway.append(button(tr('openHost', { host: handoff.host }), () => {
|
|
367
372
|
window.open(handoff.url, '_blank', 'noopener,noreferrer');
|
|
368
373
|
}, view));
|
|
369
374
|
}
|
|
@@ -386,12 +391,15 @@ export async function mountCodexAccountSecurity({ host, request, tab, capabiliti
|
|
|
386
391
|
const response = await request('verification_status', {});
|
|
387
392
|
if (!host.isConnected || tab.closed) return;
|
|
388
393
|
const state = codexVerificationState(response.verification);
|
|
389
|
-
|
|
390
|
-
verification.
|
|
391
|
-
verification.
|
|
394
|
+
const reason = response.verification?.unavailableReason;
|
|
395
|
+
verification.hidden = !!reason && reason !== 'credentialMissing';
|
|
396
|
+
if (verification.hidden) return;
|
|
397
|
+
verification.append(element('div', 'cxp-settings-section-title', tr('verification')));
|
|
398
|
+
verification.append(element('div', 'cxp-settings-hint', tr(state.enrolled ? 'credentialReady' : 'credentialMissing')));
|
|
399
|
+
verification.append(element('div', 'cxp-settings-hint', tr('localCredential')));
|
|
392
400
|
const action = state.enrolled ? 'verification_delete' : 'verification_enroll';
|
|
393
|
-
const control = button(state.enrolled ? '
|
|
394
|
-
if (state.enrolled && !window.confirm('
|
|
401
|
+
const control = button(tr(state.enrolled ? 'remove' : 'enroll'), async () => {
|
|
402
|
+
if (state.enrolled && !window.confirm(tr('removeCredential'))) return;
|
|
395
403
|
await request(action, {}); await readVerification();
|
|
396
404
|
}, view, available(action));
|
|
397
405
|
if (state.enrolled) control.style.color = 'var(--cxp-danger)';
|
|
@@ -399,7 +407,7 @@ export async function mountCodexAccountSecurity({ host, request, tab, capabiliti
|
|
|
399
407
|
if (!available(action).supported) verification.append(element('div', 'cxp-settings-hint', available(action).reason));
|
|
400
408
|
} catch (failure) {
|
|
401
409
|
if (available('verification_status').supported) {
|
|
402
|
-
verification.append(element('div', 'cxp-settings-hint',
|
|
410
|
+
verification.append(element('div', 'cxp-settings-hint', tr('error', { message: failure.message })));
|
|
403
411
|
}
|
|
404
412
|
}
|
|
405
413
|
}
|
|
@@ -41,6 +41,8 @@ export let windowId = hostedSidepanelId || sessionStorage.getItem('cxp-window-id
|
|
|
41
41
|
// ── Storage Keys ──
|
|
42
42
|
export const STOR = {
|
|
43
43
|
threadId: 'synabun-codex-panel-thread',
|
|
44
|
+
// The account that thread belongs to (its rollout lives in that account's CODEX_HOME).
|
|
45
|
+
threadAccount: 'synabun-codex-panel-thread-account',
|
|
44
46
|
title: 'synabun-codex-panel-title',
|
|
45
47
|
project: 'synabun-codex-panel-project',
|
|
46
48
|
projectThreads: 'synabun-codex-panel-project-threads',
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { storage } from '../storage.js';
|
|
5
5
|
import { state, emit, on } from '../state.js';
|
|
6
6
|
import { registerSidepanel, setSidepanelVisible, sidepanelAcceptsKeyboard, openAnotherSidepanelSession } from '../ui-sidepanel-windows.js';
|
|
7
|
-
import {
|
|
7
|
+
import { mountCliBanner } from '../ui-cli-banner.js';
|
|
8
8
|
import { nativeLoopWindowId } from '../ui-native-window-id.js';
|
|
9
9
|
import { createStickyScrollController, getStickyScrollController } from '../ui-scroll-follow.js';
|
|
10
10
|
import { wireFileLinks } from '../ui-file-links.js';
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
requestModelList, populateModelDropdown,
|
|
31
31
|
setSessionLabel, renameActiveSession, promptNameNewSession, openSettingsPanel, renderSessionMenu,
|
|
32
32
|
toggleAccountMenu,
|
|
33
|
+
mountCodexAccountMenu,
|
|
33
34
|
contextMenuData, contextMenuReading, refreshContextMenuDetails,
|
|
34
35
|
addImageFromFile, removeImage, renderImageStrip,
|
|
35
36
|
addPathChip, removePathChip, renderPathStrip,
|
|
@@ -62,8 +63,7 @@ document.addEventListener('visibilitychange', () => {
|
|
|
62
63
|
let _panel = null;
|
|
63
64
|
let _visible = false;
|
|
64
65
|
let _resizeBound = false;
|
|
65
|
-
let
|
|
66
|
-
let _cliUnsub = null;
|
|
66
|
+
let _cliBanner = null; // the CLI banner and send-block (ui-cli-banner.js)
|
|
67
67
|
|
|
68
68
|
// ═══════════════════════════════════════════
|
|
69
69
|
// Bound State (mirrors active tab — managed by cdx-tabs)
|
|
@@ -275,7 +275,6 @@ function buildPanel() {
|
|
|
275
275
|
<button class="cxp-btn" id="cxp-slide" title="Slide panel"><svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg></button>
|
|
276
276
|
</div>
|
|
277
277
|
<div class="cxp-session-menu" id="cxp-session-menu"></div>
|
|
278
|
-
<div class="cxp-account-menu" id="cxp-account-menu"></div>
|
|
279
278
|
</div>
|
|
280
279
|
<div class="cxp-messages-container" id="cxp-messages-container"></div>
|
|
281
280
|
<div class="cxp-bottom">
|
|
@@ -406,122 +405,66 @@ function startupEmptyText() {
|
|
|
406
405
|
}
|
|
407
406
|
|
|
408
407
|
// ── CLI installation status (banner + send-block) ──
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
`;
|
|
432
|
-
container.parentNode.insertBefore(banner, container);
|
|
433
|
-
const btn = banner.querySelector('.cxp-cli-banner-recheck');
|
|
434
|
-
btn?.addEventListener('click', async () => {
|
|
435
|
-
btn.disabled = true;
|
|
436
|
-
btn.textContent = 'Checking…';
|
|
437
|
-
try { await recheckCliStatus('codex'); }
|
|
438
|
-
finally {
|
|
439
|
-
if (btn.isConnected) {
|
|
440
|
-
btn.disabled = false;
|
|
441
|
-
btn.textContent = 'Re-check';
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
renderCliBannerContent(banner);
|
|
446
|
-
return banner;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
// Two very different situations reach this banner, and telling the user the
|
|
450
|
-
// CLI is "not installed" when it is installed sends them off to reinstall
|
|
451
|
-
// something that is already there. Distinguish them.
|
|
452
|
-
function renderCliBannerContent(banner) {
|
|
453
|
-
const titleEl = banner.querySelector('.cxp-cli-banner-title');
|
|
454
|
-
const bodyEl = banner.querySelector('.cxp-cli-banner-body');
|
|
455
|
-
if (!titleEl || !bodyEl) return;
|
|
456
|
-
const label = getCliLabel('codex');
|
|
457
|
-
bodyEl.replaceChildren();
|
|
458
|
-
|
|
459
|
-
if (_cliInstalled === null) {
|
|
460
|
-
titleEl.textContent = `${label} CLI not installed`;
|
|
461
|
-
const cmd = getCliInstallCommand('codex');
|
|
462
|
-
bodyEl.append('Run ');
|
|
463
|
-
const code = document.createElement('code');
|
|
464
|
-
code.textContent = cmd;
|
|
465
|
-
bodyEl.append(code, ' or follow the install guide.');
|
|
466
|
-
return;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
titleEl.textContent = `${label} CLI failed to start`;
|
|
470
|
-
bodyEl.append(`${label} v${_cliInstalled} is installed, but it could not be launched. `);
|
|
471
|
-
if (_cliFailureReason) {
|
|
472
|
-
const code = document.createElement('code');
|
|
473
|
-
code.textContent = _cliFailureReason;
|
|
474
|
-
bodyEl.append(code);
|
|
475
|
-
} else {
|
|
476
|
-
bodyEl.append('Check the CLI path in Settings, then re-check.');
|
|
408
|
+
// One controller (ui-cli-banner.js) draws the banner above the transcript while
|
|
409
|
+
// Codex is not usable: not installed (with Install, which runs OpenAI's own
|
|
410
|
+
// installer on the server), being installed, failed to install, or found but
|
|
411
|
+
// failed to start (with the error it gave). It toggles `cxp-cli-blocked` on the
|
|
412
|
+
// panel, which syncInputEnabled (cdx-tabs.js) reads to keep Send off. What it
|
|
413
|
+
// says is cli-install-view.js (`cliInstall.*` strings).
|
|
414
|
+
let _cliWasBlocked = false;
|
|
415
|
+
|
|
416
|
+
function cliBanner() {
|
|
417
|
+
if (!_cliBanner) {
|
|
418
|
+
_cliBanner = mountCliBanner({
|
|
419
|
+
toolKey: 'codex',
|
|
420
|
+
prefix: 'cxp',
|
|
421
|
+
panel: () => _panel,
|
|
422
|
+
anchor: () => panelEl('#cxp-messages-container'),
|
|
423
|
+
onBlockedChange: (blocked) => {
|
|
424
|
+
try { syncInputEnabled(); } catch {}
|
|
425
|
+
const usableAgain = _cliWasBlocked && !blocked;
|
|
426
|
+
_cliWasBlocked = blocked;
|
|
427
|
+
if (usableAgain) reconnectAfterCli();
|
|
428
|
+
},
|
|
429
|
+
});
|
|
477
430
|
}
|
|
431
|
+
return _cliBanner;
|
|
478
432
|
}
|
|
479
433
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
|
|
434
|
+
// Codex was installed (or found again) after this tab's session could not start it: the tab connects again, so
|
|
435
|
+
// Send works without reopening the panel. A tab that is running, or one an automation owns, is left alone.
|
|
436
|
+
function reconnectAfterCli() {
|
|
437
|
+
const tab = activeTab();
|
|
438
|
+
if (!tab || tab.closed || tab.automationActive || tab.running) return;
|
|
439
|
+
if (tab.connected && tab.bootstrapped) return;
|
|
440
|
+
try {
|
|
441
|
+
if (tab.ws && tab.ws.readyState === WebSocket.OPEN) disconnectTab(tab);
|
|
442
|
+
connectTab(tab);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
console.warn('[cdx-panel] reconnect after Codex became usable failed', error);
|
|
445
|
+
}
|
|
485
446
|
}
|
|
486
447
|
|
|
487
448
|
function refreshCliBanner() {
|
|
488
449
|
if (!_panel) return;
|
|
489
|
-
|
|
490
|
-
_panel.classList.toggle('cxp-cli-blocked', missing);
|
|
491
|
-
if (missing) ensureCliBannerEl();
|
|
492
|
-
else removeCliBannerEl();
|
|
450
|
+
cliBanner().refresh();
|
|
493
451
|
try { syncInputEnabled(); } catch {}
|
|
494
452
|
}
|
|
495
453
|
|
|
496
454
|
export function isCdxCliInstalled() {
|
|
497
|
-
return
|
|
455
|
+
return !cliBanner().blocked();
|
|
498
456
|
}
|
|
499
457
|
|
|
458
|
+
// A session could not start Codex: "failed to start" with its words at once,
|
|
459
|
+
// then the server is asked again and its answer decides what stays. (The
|
|
460
|
+
// answer is read from the re-check itself: a spawn failure does not change
|
|
461
|
+
// the version, so a change-only notification would never clear the banner.)
|
|
500
462
|
export function flagCdxCliInstallFailure(reason = null) {
|
|
501
|
-
|
|
502
|
-
_cliFailureReason = reason ? String(reason).slice(0, 300) : null;
|
|
503
|
-
refreshCliBanner();
|
|
504
|
-
// Clear the latch from the value recheckCliStatus RETURNS. Relying on the
|
|
505
|
-
// subscriber callback alone deadlocked: the callback only fired when the
|
|
506
|
-
// version string changed, and a spawn failure does not change the version,
|
|
507
|
-
// so the banner could never be dismissed — the Re-check button included.
|
|
508
|
-
recheckCliStatus('codex')
|
|
509
|
-
.then((info) => {
|
|
510
|
-
if (!info?.installed) return;
|
|
511
|
-
_cliInstallFailureForced = false;
|
|
512
|
-
_cliFailureReason = null;
|
|
513
|
-
refreshCliBanner();
|
|
514
|
-
})
|
|
515
|
-
.catch(() => {});
|
|
463
|
+
cliBanner().flagFailure(reason);
|
|
516
464
|
}
|
|
517
465
|
|
|
518
466
|
function ensureCliSubscription() {
|
|
519
|
-
|
|
520
|
-
_cliUnsub = subscribeCliStatus('codex', (info) => {
|
|
521
|
-
_cliInstalled = info?.installed || null;
|
|
522
|
-
if (_cliInstalled) { _cliInstallFailureForced = false; _cliFailureReason = null; }
|
|
523
|
-
refreshCliBanner();
|
|
524
|
-
});
|
|
467
|
+
cliBanner();
|
|
525
468
|
}
|
|
526
469
|
|
|
527
470
|
// ═══════════════════════════════════════════
|
|
@@ -906,7 +849,6 @@ function wireEvents() {
|
|
|
906
849
|
},
|
|
907
850
|
onOpen: () => {
|
|
908
851
|
sessionMenu?.classList.remove('open');
|
|
909
|
-
panelEl('#cxp-account-menu')?.classList.remove('open');
|
|
910
852
|
panelEl('#cxp-effort-menu')?.classList.remove('open');
|
|
911
853
|
refreshContextMenuDetails();
|
|
912
854
|
},
|
|
@@ -916,8 +858,8 @@ function wireEvents() {
|
|
|
916
858
|
// After this click has reached the document, whose handler closes the menu.
|
|
917
859
|
onAccount: () => setTimeout(() => toggleAccountMenu(), 0),
|
|
918
860
|
});
|
|
919
|
-
// The account menu
|
|
920
|
-
panelEl('
|
|
861
|
+
// The header account menu (account-menu.js, shared with the Claude panel): before the cog.
|
|
862
|
+
mountCodexAccountMenu(panelEl('.cxp-actions'), _panel);
|
|
921
863
|
const slideBtn = panelEl('#cxp-slide');
|
|
922
864
|
slideBtn?.addEventListener('click', () => setVisible(false));
|
|
923
865
|
const changelogBtn = panelEl('#cxp-action-changelog');
|
|
@@ -1035,7 +977,6 @@ function wireEvents() {
|
|
|
1035
977
|
});
|
|
1036
978
|
document.addEventListener('click', (event) => {
|
|
1037
979
|
if (!event.target.closest('.cxp-header')) sessionMenu?.classList.remove('open');
|
|
1038
|
-
if (!event.target.closest('#cxp-account-menu')) panelEl('#cxp-account-menu')?.classList.remove('open');
|
|
1039
980
|
if (!event.target.closest('.cxp-dropdown')) {
|
|
1040
981
|
_panel?.querySelectorAll('.cxp-dropdown.open').forEach((node) => node.classList.remove('open'));
|
|
1041
982
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { t, getLocale } from '../i18n.js';
|
|
2
|
+
import { accountSummary } from '../account-view.js';
|
|
3
|
+
import { configChanges, configRestriction, configValue, SESSION_CONFIG_FIELDS, sessionSettings, modelServiceTiers, capabilityAvailability } from './cdx-commands.js';
|
|
4
|
+
import { mountCodexAccountSecurity } from './cdx-features.js';
|
|
5
|
+
|
|
6
|
+
const overrideValue = (field, value) => field === 'sandboxPolicy' && value ? ({ readOnly: 'read-only', workspaceWrite: 'workspace-write', dangerFullAccess: 'danger-full-access' })[value.type] ?? null : value;
|
|
7
|
+
const tr = (key, params) => t(`codexSettings.${key}`, params);
|
|
8
|
+
const el = (tag, cls, text) => { const node = document.createElement(tag); if (cls) node.className = cls; if (text != null) node.textContent = text; return node; };
|
|
9
|
+
const metadata = [
|
|
10
|
+
['model', 'model', 'models'], ['review_model', 'reviewModel', 'models'],
|
|
11
|
+
['model_reasoning_effort', 'effort', 'models'], ['model_reasoning_summary', 'summary', 'models'],
|
|
12
|
+
['service_tier', 'serviceTier', 'models'], ['personality', 'personality', 'models'],
|
|
13
|
+
['approval_policy', 'approval', 'permissions'], ['sandbox_mode', 'sandbox', 'permissions'],
|
|
14
|
+
['web_search', 'webSearch', 'tools'], ['model_provider', 'provider', 'advanced'],
|
|
15
|
+
['model_verbosity', 'verbosity', 'advanced'], ['model_context_window', 'contextWindow', 'advanced'],
|
|
16
|
+
['model_auto_compact_token_limit', 'autoCompact', 'advanced'],
|
|
17
|
+
];
|
|
18
|
+
export function codexSettingsDefinitions(schema, models, cfg) {
|
|
19
|
+
const model = models.find(m => (m.model || m.id) === cfg.model) || models.find(m => m.isDefault) || models[0];
|
|
20
|
+
const modelOptions = models.map(m => ({ value: m.model || m.id, label: m.displayName || m.label || m.model || m.id }));
|
|
21
|
+
return metadata.flatMap(([key, label, section]) => {
|
|
22
|
+
const field = schema.fields?.[key]; if (!field) return [];
|
|
23
|
+
if (key === 'service_tier' && cfg.model_provider && cfg.model_provider !== 'openai') return [];
|
|
24
|
+
let options = (field.values || []).map(value => ({ value, label: tr(`values.${value}`) === `codexSettings.values.${value}` ? value : tr(`values.${value}`) }));
|
|
25
|
+
if (key === 'model' || key === 'review_model') options = modelOptions;
|
|
26
|
+
if (key === 'model_reasoning_effort') options = (model?.supportedReasoningEfforts || []).map(e => ({ value: e.reasoningEffort, label: e.reasoningEffort }));
|
|
27
|
+
if (key === 'service_tier') options = modelServiceTiers(model);
|
|
28
|
+
if (key === 'personality' && !model?.supportsPersonality) return [];
|
|
29
|
+
// The provider list is configured locally. Built-in OpenAI plus named providers.
|
|
30
|
+
if (key === 'model_provider') options = [...new Set(['openai', ...Object.keys(cfg.model_providers || {}), cfg.model_provider].filter(Boolean))].map(value => ({ value, label: value }));
|
|
31
|
+
if (field.type !== 'number' && !options.length) return [];
|
|
32
|
+
return [{ key, label: tr(label), section, type: field.type, options: [{ value: '', label: tr('default') }, ...options] }];
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function mountCodexSettings({ host, tab, request, capabilities, account, models, normalizeConfig, manageAccount, authenticate, onSaved }) {
|
|
37
|
+
if (host.querySelector('.cxp-settings-overlay')) return;
|
|
38
|
+
const previousFocus = document.activeElement;
|
|
39
|
+
const overlay = el('div', 'cxp-settings-overlay');
|
|
40
|
+
const panel = el('div', 'cxp-settings-panel'); panel.setAttribute('role', 'dialog'); panel.setAttribute('aria-modal', 'true'); panel.setAttribute('aria-label', tr('title'));
|
|
41
|
+
const header = el('div', 'cxp-settings-header');
|
|
42
|
+
const close = el('button', 'cxp-settings-close', '×'); close.setAttribute('aria-label', tr('close'));
|
|
43
|
+
const dismiss = () => { overlay.remove(); if (previousFocus?.isConnected) previousFocus.focus(); };
|
|
44
|
+
close.onclick = dismiss; header.append(el('span', '', tr('title')), close);
|
|
45
|
+
const body = el('div', 'cxp-settings-body'); const sections = {};
|
|
46
|
+
for (const key of ['account', 'models', 'permissions', 'tools', 'advanced']) {
|
|
47
|
+
const group = el(key === 'advanced' ? 'details' : 'section', 'cxp-settings-section'); group.dataset.section = key;
|
|
48
|
+
group.append(el(key === 'advanced' ? 'summary' : 'h3', 'cxp-settings-section-title', tr(key)));
|
|
49
|
+
const content = el('div', 'cxp-settings-group'); group.append(content); sections[key] = content; body.append(group);
|
|
50
|
+
}
|
|
51
|
+
const result = el('div', 'cxp-settings-result'); result.setAttribute('role', 'status');
|
|
52
|
+
const footer = el('div', 'cxp-settings-footer');
|
|
53
|
+
panel.append(header, body, footer); overlay.append(panel); host.append(overlay);
|
|
54
|
+
const error = message => { result.textContent = tr('error', { message }); result.dataset.error = 'true'; };
|
|
55
|
+
overlay.onclick = event => { if (event.target === overlay) dismiss(); };
|
|
56
|
+
overlay.onkeydown = event => {
|
|
57
|
+
if (event.key === 'Escape') { event.stopPropagation(); dismiss(); }
|
|
58
|
+
if (event.key === 'Tab') {
|
|
59
|
+
const controls = [...panel.querySelectorAll('button,select,input,summary,a[href]')].filter(n => !n.disabled && n.getClientRects().length);
|
|
60
|
+
if (event.shiftKey && document.activeElement === controls[0]) { event.preventDefault(); controls.at(-1)?.focus(); }
|
|
61
|
+
else if (!event.shiftKey && document.activeElement === controls.at(-1)) { event.preventDefault(); controls[0]?.focus(); }
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
close.focus();
|
|
65
|
+
footer.append(result);
|
|
66
|
+
const action = (label, fn) => {
|
|
67
|
+
const button = el('button', 'cxp-btn cxp-btn-sm', label); button.type = 'button';
|
|
68
|
+
button.onclick = async () => { button.disabled = true; try { await fn(); } catch (failure) { error(failure.message); } finally { if (button.isConnected) button.disabled = false; } };
|
|
69
|
+
return button;
|
|
70
|
+
};
|
|
71
|
+
sections.account.append(el('div', 'cxp-settings-loading', tr('loading')));
|
|
72
|
+
try {
|
|
73
|
+
const [configData, mcpData, permissions, limits, requirements, accountData] = await Promise.allSettled([
|
|
74
|
+
request('config_read', { cwd: tab.project || null, threadId: tab.threadId || null }), request('mcp_status'),
|
|
75
|
+
request('permission_roots_list'), request('rate_limits_read'), request('config_requirements'),
|
|
76
|
+
request('account_read'),
|
|
77
|
+
]);
|
|
78
|
+
if (!overlay.isConnected || tab.closed) return;
|
|
79
|
+
sections.account.replaceChildren();
|
|
80
|
+
const details = el('div', 'cxp-settings-account-details'); details.dataset.codexAccountDetails = 'true';
|
|
81
|
+
const currentAccount = accountData.status === 'fulfilled' ? (accountData.value?.accountState || account) : account;
|
|
82
|
+
details.append(el('div', 'cxp-settings-value', currentAccount ? accountSummary(currentAccount, 'codex') : tr('defaultAccount')));
|
|
83
|
+
const rateLimits = limits.status === 'fulfilled' ? limits.value?.rateLimits : null;
|
|
84
|
+
const buckets = Object.values(rateLimits?.rateLimitsByLimitId || {});
|
|
85
|
+
if (!buckets.length && rateLimits?.rateLimits) buckets.push(rateLimits.rateLimits);
|
|
86
|
+
for (const bucket of buckets) {
|
|
87
|
+
const percent = bucket.primary?.usedPercent;
|
|
88
|
+
if (!Number.isFinite(percent)) continue;
|
|
89
|
+
const time = bucket.primary?.resetsAt ? new Date(bucket.primary.resetsAt * 1000).toLocaleString(getLocale()) : tr('unknown');
|
|
90
|
+
details.append(el('div', 'cxp-settings-hint', tr('usage', { name: bucket.limitName || bucket.limitId || 'Codex', percent: Math.round(percent), time })));
|
|
91
|
+
}
|
|
92
|
+
details.append(action(tr('manage'), () => { dismiss(); manageAccount(); })); sections.account.append(details);
|
|
93
|
+
await mountCodexAccountSecurity({ host: sections.account, experimentalHost: sections.advanced, request, tab, capabilities });
|
|
94
|
+
const runtime = tab.codexRuntime || {};
|
|
95
|
+
sections.advanced.append(el('div', 'cxp-settings-hint cxp-settings-runtime', tr('runtime', { cli: runtime.cliVersion || tr('unknown'), sdk: runtime.sdkVersion || tr('unknown'), protocol: runtime.protocolBaseline || tr('unknown') })));
|
|
96
|
+
if (runtime.codexBin) sections.advanced.append(el('div', 'cxp-settings-hint', runtime.codexBin));
|
|
97
|
+
const mcp = el('div', ''); mcp.id = 'cxp-settings-mcp'; sections.tools.append(el('h4', 'cxp-settings-subtitle', tr('mcp')), mcp);
|
|
98
|
+
async function renderMcp(data) {
|
|
99
|
+
if (data.error) throw new Error(data.error);
|
|
100
|
+
mcp.replaceChildren();
|
|
101
|
+
for (const server of data.servers || []) {
|
|
102
|
+
const row = el('div', 'cxp-mcp-row');
|
|
103
|
+
const statusKey = server.requiresOAuth ? 'loginRequired' : server.status === 'running' ? 'running' : 'configured';
|
|
104
|
+
const status = el('span', 'cxp-mcp-status', tr(statusKey)); status.dataset.status = server.status;
|
|
105
|
+
row.append(el('span', 'cxp-mcp-name', server.name), status);
|
|
106
|
+
if (server.alwaysAllowed) row.append(el('span', 'cxp-mcp-status', tr('alwaysAllowed')));
|
|
107
|
+
row.append(
|
|
108
|
+
action(tr('refresh'), async () => { await request('mcp_refresh', { name: server.name }); await renderMcp(await request('mcp_status')); }));
|
|
109
|
+
if (server.alwaysAllowed) row.append(action(tr('revoke'), async () => {
|
|
110
|
+
await request('mcp_approval_set', { name: server.name, approvalMode: 'prompt' }); await renderMcp(await request('mcp_status'));
|
|
111
|
+
}));
|
|
112
|
+
if (server.requiresOAuth) row.append(action(tr('authenticate'), () => authenticate(server.name)));
|
|
113
|
+
mcp.append(row);
|
|
114
|
+
}
|
|
115
|
+
if (!mcp.childElementCount) mcp.append(el('div', 'cxp-settings-hint', tr('noMcp')));
|
|
116
|
+
}
|
|
117
|
+
if (mcpData.status === 'fulfilled') await renderMcp(mcpData.value); else error(mcpData.reason.message);
|
|
118
|
+
const roots = el('div', ''); roots.id = 'cxp-settings-permissions';
|
|
119
|
+
sections.permissions.append(el('h4', 'cxp-settings-subtitle', tr('folders')), roots);
|
|
120
|
+
async function renderRoots(data) {
|
|
121
|
+
if (data.error) throw new Error(data.error);
|
|
122
|
+
roots.replaceChildren(); const fs = data.permissions?.filesystem || {};
|
|
123
|
+
for (const access of ['write', 'read']) for (const path of fs[access] || []) {
|
|
124
|
+
const row = el('div', 'cxp-mcp-row'); const name = el('span', 'cxp-mcp-name cxp-permission-root-path', path); name.title = path;
|
|
125
|
+
row.append(name, el('span', 'cxp-mcp-status', tr(access)), action(tr('revoke'), async () => {
|
|
126
|
+
await request('permission_root_remove', { access, path }); await renderRoots(await request('permission_roots_list'));
|
|
127
|
+
})); roots.append(row);
|
|
128
|
+
}
|
|
129
|
+
if (!roots.childElementCount) roots.append(el('div', 'cxp-settings-hint', tr('none')));
|
|
130
|
+
}
|
|
131
|
+
if (permissions.status === 'fulfilled') await renderRoots(permissions.value); else error(permissions.reason.message);
|
|
132
|
+
if (configData.status !== 'fulfilled' || configData.value.error) throw new Error(configData.value?.error || configData.reason.message);
|
|
133
|
+
let cfg = normalizeConfig(configData.value.config).effective;
|
|
134
|
+
let overrides = Object.fromEntries(Object.entries(SESSION_CONFIG_FIELDS).filter(([, field]) => Object.hasOwn(configData.value.sessionOverrides || {}, field)).map(([key, field]) => [key, overrideValue(field, configData.value.sessionOverrides[field])]));
|
|
135
|
+
const schema = configData.value.settingsSchema;
|
|
136
|
+
if (!schema?.fields) throw new Error(tr('noSchema'));
|
|
137
|
+
const modelList = await models();
|
|
138
|
+
let defs = [], inputs = {}, restrictions = {};
|
|
139
|
+
const fields = {};
|
|
140
|
+
for (const key of ['models', 'permissions', 'tools', 'advanced']) { fields[key] = el('div', 'cxp-config-form'); sections[key].prepend(fields[key]); }
|
|
141
|
+
const scopeRow = el('label', 'cxp-config-row cxp-settings-scope'); scopeRow.append(el('span', '', tr('applyTo')));
|
|
142
|
+
const scope = el('select', 'cxp-config-input'); scope.id = 'cxp-settings-scope'; scope.setAttribute('aria-label', tr('applyTo'));
|
|
143
|
+
for (const [value, label] of [['saved', 'savedDefaults'], ['session', 'currentThread']]) { const o = el('option', '', tr(label)); o.value = value; o.disabled = value === 'session' && (!tab.threadId || !capabilityAvailability(capabilities, 'thread_settings_update').supported); scope.append(o); }
|
|
144
|
+
scopeRow.append(scope); body.prepend(scopeRow);
|
|
145
|
+
const scopeHint = el('div', 'cxp-settings-hint cxp-settings-scope-hint', tr('scopeHint')); scopeRow.after(scopeHint);
|
|
146
|
+
const featureHost = el('details', 'cxp-settings-features'); featureHost.append(el('summary', 'cxp-settings-subtitle', tr('features'))); sections.tools.append(featureHost);
|
|
147
|
+
const featureInputs = {};
|
|
148
|
+
for (const feature of schema.features || []) {
|
|
149
|
+
if (!['stable', 'beta'].includes(feature.stage) || feature.configurable === false) continue;
|
|
150
|
+
const row = el('label', 'cxp-config-row'); row.append(el('span', 'cxp-config-label', feature.name));
|
|
151
|
+
const input = el('button', 'cxp-settings-toggle'); input.type = 'button'; input.setAttribute('role', 'switch'); input.setAttribute('aria-label', feature.name); input.dataset.feature = feature.name;
|
|
152
|
+
const restriction = configRestriction(requirements.status === 'fulfilled' ? requirements.value : {}, `features.${feature.name}`);
|
|
153
|
+
input.dataset.fixed = String(typeof restriction.fixed === 'boolean');
|
|
154
|
+
const set = value => { input.dataset.enabled = String(value); input.setAttribute('aria-checked', String(value)); input.textContent = tr(value ? 'on' : 'off'); };
|
|
155
|
+
set(typeof restriction.fixed === 'boolean' ? restriction.fixed : feature.enabled);
|
|
156
|
+
input.onclick = () => set(input.dataset.enabled !== 'true'); row.append(input); featureHost.append(row); featureInputs[feature.name] = { input, feature, set };
|
|
157
|
+
}
|
|
158
|
+
const save = action(tr('saveDefaults'), async () => {
|
|
159
|
+
result.textContent = tr('saving'); delete result.dataset.error;
|
|
160
|
+
const current = scope.value === 'session' ? { ...cfg, ...overrides } : cfg;
|
|
161
|
+
const patch = configChanges(defs, inputs, current);
|
|
162
|
+
let featureEdits = {};
|
|
163
|
+
if (scope.value === 'session') {
|
|
164
|
+
if (tab.running) throw new Error(tr('busy'));
|
|
165
|
+
if (Object.keys(patch).length) await request('thread_settings_update', { threadId: tab.threadId, cwd: tab.project || null, ...sessionSettings(patch) });
|
|
166
|
+
} else {
|
|
167
|
+
const enablement = Object.fromEntries(Object.values(featureInputs).filter(({ input, feature }) => !input.disabled && (input.dataset.enabled === 'true') !== feature.enabled).map(({ input, feature }) => [feature.name, input.dataset.enabled === 'true']));
|
|
168
|
+
featureEdits = enablement;
|
|
169
|
+
const config = { ...patch, ...Object.fromEntries(Object.entries(enablement).map(([key, value]) => [`features.${key}`, value])) };
|
|
170
|
+
if (Object.keys(config).length) await request('config_write', { config, cwd: tab.project || null });
|
|
171
|
+
}
|
|
172
|
+
// Always re-read the runtime. Never report success from an optimistic UI copy.
|
|
173
|
+
const read = await request('config_read', { cwd: tab.project || null, threadId: tab.threadId || null });
|
|
174
|
+
if (read.error) throw new Error(read.error);
|
|
175
|
+
cfg = normalizeConfig(read.config).effective;
|
|
176
|
+
overrides = Object.fromEntries(Object.entries(SESSION_CONFIG_FIELDS).filter(([, field]) => Object.hasOwn(read.sessionOverrides || {}, field)).map(([key, field]) => [key, overrideValue(field, read.sessionOverrides[field])]));
|
|
177
|
+
for (const { input, feature, set } of Object.values(featureInputs)) {
|
|
178
|
+
const actual = read.settingsSchema?.features?.find(f => f.name === feature.name);
|
|
179
|
+
if (actual) { feature.enabled = actual.enabled; set(actual.enabled); }
|
|
180
|
+
}
|
|
181
|
+
renderFields(); onSaved({ scope: scope.value, patch, overrides });
|
|
182
|
+
const effective = scope.value === 'session' ? { ...cfg, ...overrides } : cfg;
|
|
183
|
+
const unapplied = Object.entries(patch).filter(([key, value]) => JSON.stringify(configValue(effective, key) ?? null) !== JSON.stringify(value));
|
|
184
|
+
for (const [key, value] of Object.entries(featureEdits)) if (featureInputs[key]?.feature.enabled !== value) unapplied.push([key]);
|
|
185
|
+
if (unapplied.length) throw new Error(tr('notApplied', { keys: unapplied.map(([key]) => key).join(', ') }));
|
|
186
|
+
result.textContent = tr('saved');
|
|
187
|
+
}); save.classList.add('cxp-config-save'); footer.prepend(save);
|
|
188
|
+
function renderFields(selection) {
|
|
189
|
+
const visible = scope.value === 'session' ? { ...cfg, ...overrides } : cfg;
|
|
190
|
+
const values = selection || visible;
|
|
191
|
+
defs = codexSettingsDefinitions(schema, modelList, values); inputs = {}; restrictions = {};
|
|
192
|
+
for (const node of Object.values(fields)) node.replaceChildren();
|
|
193
|
+
for (const def of defs) {
|
|
194
|
+
if (scope.value === 'session' && !SESSION_CONFIG_FIELDS[def.key]) continue;
|
|
195
|
+
const row = el('div', 'cxp-config-row'); const label = el('label', 'cxp-config-label', def.label);
|
|
196
|
+
const input = el(def.type === 'number' ? 'input' : 'select', 'cxp-config-input');
|
|
197
|
+
input.id = `cxp-setting-${def.key}`; input.dataset.configKey = def.key; label.htmlFor = input.id;
|
|
198
|
+
const value = configValue(values, def.key);
|
|
199
|
+
if (def.type === 'number') { input.type = 'number'; input.min = '1'; input.step = '1'; input.placeholder = tr('default'); }
|
|
200
|
+
else {
|
|
201
|
+
for (const option of def.options) { const o = el('option', '', option.label); o.value = option.value; input.append(o); }
|
|
202
|
+
if (value != null && !def.options.some(o => o.value === String(value))) {
|
|
203
|
+
const o = el('option', '', tr('configured', { value: typeof value === 'object' ? JSON.stringify(value) : value })); o.value = typeof value === 'object' ? JSON.stringify(value) : String(value); input.append(o); input.dataset.originalValue = JSON.stringify(value);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
input.value = value == null ? '' : typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
207
|
+
const restriction = configRestriction(requirements.status === 'fulfilled' ? requirements.value : {}, def.key);
|
|
208
|
+
restrictions[def.key] = restriction;
|
|
209
|
+
input.disabled = Object.hasOwn(restriction, 'fixed');
|
|
210
|
+
for (const o of input.options || []) if (o.value && restriction.allowed && !restriction.allowed.some(v => JSON.stringify(v) === JSON.stringify(o.value))) o.disabled = true;
|
|
211
|
+
const layer = normalizeConfig(configData.value.config).layers[def.key];
|
|
212
|
+
if (layer || restriction.reason) input.title = tr(restriction.reason ? 'managed' : 'source', { layer });
|
|
213
|
+
inputs[def.key] = input; row.append(label, input);
|
|
214
|
+
if (def.type === 'number') row.append(el('span', 'cxp-settings-unit', tr('tokens')));
|
|
215
|
+
fields[def.section].append(row);
|
|
216
|
+
}
|
|
217
|
+
const modelInput = inputs.model;
|
|
218
|
+
if (modelInput) modelInput.onchange = () => {
|
|
219
|
+
const draft = { ...values };
|
|
220
|
+
for (const def of defs) if (inputs[def.key]) draft[def.key] = inputs[def.key].value || null;
|
|
221
|
+
draft.model_reasoning_effort = null; draft.service_tier = null; renderFields(draft);
|
|
222
|
+
};
|
|
223
|
+
for (const { input } of Object.values(featureInputs)) input.disabled = scope.value === 'session' || input.dataset.fixed === 'true';
|
|
224
|
+
featureHost.hidden = scope.value === 'session';
|
|
225
|
+
for (const key of ['models', 'permissions', 'tools', 'advanced']) sections[key].closest('.cxp-settings-section').hidden = scope.value === 'session' && !fields[key].childElementCount;
|
|
226
|
+
save.textContent = tr(scope.value === 'session' ? 'applyThread' : 'saveDefaults');
|
|
227
|
+
}
|
|
228
|
+
scope.onchange = () => renderFields(); renderFields();
|
|
229
|
+
} catch (failure) { error(failure.message); }
|
|
230
|
+
return { overlay, sections };
|
|
231
|
+
}
|