@oar-sh/oar 0.9.0
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/.github/extensions/web-relay/README.md +190 -0
- package/.github/extensions/web-relay/extension.mjs +972 -0
- package/.github/extensions/web-relay/model-api/model-switching.mjs +338 -0
- package/.github/extensions/web-relay/polling/heartbeat.mjs +55 -0
- package/.github/extensions/web-relay/polling/polling-loop.mjs +1105 -0
- package/.github/extensions/web-relay/runtime/api-client.mjs +34 -0
- package/.github/extensions/web-relay/runtime/banner-state.mjs +126 -0
- package/.github/extensions/web-relay/runtime/config-loader.mjs +104 -0
- package/.github/extensions/web-relay/runtime/debug-log.mjs +27 -0
- package/.github/extensions/web-relay/runtime/send-and-wait-errors.mjs +170 -0
- package/.github/extensions/web-relay/runtime/session-binding.mjs +78 -0
- package/.github/extensions/web-relay/runtime/session-io.mjs +349 -0
- package/.github/extensions/web-relay/runtime/session-join-retry.mjs +44 -0
- package/.github/extensions/web-relay/runtime/session-registry.mjs +29 -0
- package/.github/extensions/web-relay/runtime/session-runtime-manager.mjs +156 -0
- package/.github/extensions/web-relay/runtime/session-sync-bridge.mjs +74 -0
- package/.github/extensions/web-relay/runtime/worker-websocket-link.mjs +325 -0
- package/.github/extensions/web-relay/runtime/workspace-root.mjs +55 -0
- package/.github/extensions/web-relay/server-lifecycle/managed-server.mjs +386 -0
- package/.github/extensions/web-relay/skills/prompt-context.mjs +160 -0
- package/.github/extensions/web-relay/skills/question-bridge.mjs +141 -0
- package/.github/extensions/web-relay/skills/question-routing-hooks.mjs +463 -0
- package/.github/extensions/web-relay/skills/reasoning-stream.mjs +285 -0
- package/.github/extensions/web-relay/skills/subagent-lifecycle.mjs +345 -0
- package/.github/extensions/web-relay/skills/tool-activity.mjs +243 -0
- package/.github/extensions/web-relay/utils/process-utils.mjs +25 -0
- package/.github/extensions/web-relay/utils/token-generation.mjs +58 -0
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +717 -0
- package/bin/bat/oar.bat +46 -0
- package/bin/oar.js +665 -0
- package/package.json +68 -0
- package/server/README.md +1379 -0
- package/server/claude-worker/claude-attachments.mjs +134 -0
- package/server/claude-worker/claude-preview-tool.mjs +57 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +420 -0
- package/server/claude-worker/claude-session-process.mjs +2391 -0
- package/server/claude-worker/claude-session-test-harness.mjs +236 -0
- package/server/claude-worker/claude-session-worker.mjs +186 -0
- package/server/claude-worker/claude-transcript-relocator.mjs +189 -0
- package/server/claude-worker/claude-turn-publisher.mjs +291 -0
- package/server/claude-worker/fixtures/workflow-run-record.json +1 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +629 -0
- package/server/claude-worker/workflow-progress-digest.mjs +269 -0
- package/server/copilot-sdk-runtime.mjs +126 -0
- package/server/copilot-worker/copilot-attachments.mjs +97 -0
- package/server/copilot-worker/copilot-byok-provider.mjs +121 -0
- package/server/copilot-worker/copilot-continuation-signals.mjs +349 -0
- package/server/copilot-worker/copilot-plan-board.mjs +110 -0
- package/server/copilot-worker/copilot-prompt-context.mjs +86 -0
- package/server/copilot-worker/copilot-question-bridge.mjs +216 -0
- package/server/copilot-worker/copilot-sdk-adapter.mjs +494 -0
- package/server/copilot-worker/copilot-sdk-event-normalizer.mjs +800 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1729 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +167 -0
- package/server/copilot-worker/copilot-sdk-test-harness.mjs +329 -0
- package/server/copilot-worker/fixtures/abort-turn.json +588 -0
- package/server/copilot-worker/fixtures/ask-user-turn.json +1154 -0
- package/server/copilot-worker/fixtures/background-timer-continuation.json +445 -0
- package/server/copilot-worker/fixtures/background-timer-turn.json +497 -0
- package/server/copilot-worker/fixtures/happy-turn.json +566 -0
- package/server/copilot-worker/fixtures/quota-turn.json +647 -0
- package/server/copilot-worker/fixtures/reasoning-turn.json +625 -0
- package/server/copilot-worker/fixtures/resume-turn.json +700 -0
- package/server/copilot-worker/fixtures/subagent-turn.json +1378 -0
- package/server/copilot-worker/fixtures/tool-permission-turn.json +1293 -0
- package/server/cursor-worker/cursor-ask-user-tool.mjs +83 -0
- package/server/cursor-worker/cursor-attachments.mjs +88 -0
- package/server/cursor-worker/cursor-context-usage.mjs +85 -0
- package/server/cursor-worker/cursor-preview-tool.mjs +44 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +554 -0
- package/server/cursor-worker/cursor-session-worker.mjs +145 -0
- package/server/cursor-worker/cursor-subagent-roster.mjs +81 -0
- package/server/cursor-worker/cursor-turn-runner.mjs +717 -0
- package/server/cursor-worker/sdk-message-normalizer.mjs +485 -0
- package/server/db-schema.mjs +932 -0
- package/server/features.mjs +106 -0
- package/server/grok-worker/acp-client.mjs +434 -0
- package/server/grok-worker/acp-host-services.mjs +277 -0
- package/server/grok-worker/grok-context-usage.mjs +77 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +334 -0
- package/server/grok-worker/grok-session-worker.mjs +145 -0
- package/server/grok-worker/grok-turn-runner.mjs +523 -0
- package/server/grok-worker/sdk-message-normalizer.mjs +311 -0
- package/server/migrations/0001-add-structured-answer.mjs +80 -0
- package/server/migrations/0002-recent-workspace-roots-path-key.mjs +121 -0
- package/server/migrations/0003-push-subscriptions.mjs +87 -0
- package/server/public/app/action-confirmations.js +267 -0
- package/server/public/app/activity-replay-state.mjs +149 -0
- package/server/public/app/api-client.js +1082 -0
- package/server/public/app/ask-user-view.js +545 -0
- package/server/public/app/attachments-view.js +2112 -0
- package/server/public/app/auto-compact-window-options.mjs +59 -0
- package/server/public/app/background-tasks-view.mjs +494 -0
- package/server/public/app/bootstrap.js +4347 -0
- package/server/public/app/claude-auth-ui.js +394 -0
- package/server/public/app/claude-thinking-options.mjs +38 -0
- package/server/public/app/cli-install-ui.js +455 -0
- package/server/public/app/code-copy.mjs +85 -0
- package/server/public/app/composer-attachment-cache.mjs +151 -0
- package/server/public/app/composer-control-state.mjs +107 -0
- package/server/public/app/composer-paste.mjs +132 -0
- package/server/public/app/context-tier-options.mjs +68 -0
- package/server/public/app/context-usage-view.mjs +338 -0
- package/server/public/app/conversation-draft-timestamp-utils.mjs +11 -0
- package/server/public/app/conversation-preferences.mjs +91 -0
- package/server/public/app/conversation-provider-indicator.mjs +88 -0
- package/server/public/app/conversation-view.js +2898 -0
- package/server/public/app/cwd-menu-placement.mjs +85 -0
- package/server/public/app/cwd-picker.js +605 -0
- package/server/public/app/emoji-view.js +101 -0
- package/server/public/app/external-link-policy.mjs +80 -0
- package/server/public/app/file-preview-navigation.mjs +51 -0
- package/server/public/app/font-scaling.js +262 -0
- package/server/public/app/git-changes-view.js +339 -0
- package/server/public/app/git-diff-model.mjs +109 -0
- package/server/public/app/grok-auth-ui.js +387 -0
- package/server/public/app/image-downscale.mjs +174 -0
- package/server/public/app/in-flight-snapshot.mjs +40 -0
- package/server/public/app/infinite-loader.js +164 -0
- package/server/public/app/journal-view.js +1141 -0
- package/server/public/app/known-cwd-options.mjs +41 -0
- package/server/public/app/launch-cwd-path.mjs +23 -0
- package/server/public/app/live-message-dedupe.mjs +57 -0
- package/server/public/app/message-search-view.js +242 -0
- package/server/public/app/model-selector-options.mjs +31 -0
- package/server/public/app/model-vision-support.mjs +56 -0
- package/server/public/app/new-conversation-model-choice.mjs +67 -0
- package/server/public/app/plan-usage-view.mjs +307 -0
- package/server/public/app/preview-cards.mjs +273 -0
- package/server/public/app/preview-command.mjs +103 -0
- package/server/public/app/push-settings.js +370 -0
- package/server/public/app/pwa-install.js +456 -0
- package/server/public/app/question-schema-view.mjs +71 -0
- package/server/public/app/reasoning-effort-labels.mjs +42 -0
- package/server/public/app/relay-board-view.js +117 -0
- package/server/public/app/relay-dot-state.mjs +69 -0
- package/server/public/app/relay-error-ctas.mjs +52 -0
- package/server/public/app/relay-prompt-sanitizer.mjs +114 -0
- package/server/public/app/relay-thoughts.mjs +98 -0
- package/server/public/app/relay-watchdog-policy.mjs +29 -0
- package/server/public/app/repo-browser-preferences.mjs +79 -0
- package/server/public/app/repo-browser-tree-state.mjs +97 -0
- package/server/public/app/router.js +657 -0
- package/server/public/app/selection-guard.mjs +93 -0
- package/server/public/app/settings-modal.js +1484 -0
- package/server/public/app/settings-tabs.js +129 -0
- package/server/public/app/slash-autocomplete.mjs +188 -0
- package/server/public/app/slash-commands.mjs +183 -0
- package/server/public/app/socket-handlers.js +703 -0
- package/server/public/app/status-store.mjs +242 -0
- package/server/public/app/status-view.mjs +315 -0
- package/server/public/app/store.js +1454 -0
- package/server/public/app/stream-state.mjs +73 -0
- package/server/public/app/streaming-dom-patch.mjs +23 -0
- package/server/public/app/sync-outbox.mjs +208 -0
- package/server/public/app/tap-actions.js +62 -0
- package/server/public/app/thread-order.mjs +54 -0
- package/server/public/app/tmux-inspector-view.js +422 -0
- package/server/public/app/transcript-separators.mjs +282 -0
- package/server/public/app-icon-192.png +0 -0
- package/server/public/app-icon-512.png +0 -0
- package/server/public/app-icon.svg +4 -0
- package/server/public/favicon.ico +0 -0
- package/server/public/index.html +5743 -0
- package/server/public/manifest.webmanifest +32 -0
- package/server/public/sw.js +307 -0
- package/server/relay-exit-codes.mjs +4 -0
- package/server/relay-self-restart.mjs +170 -0
- package/server/relay-tools.md +27 -0
- package/server/relay.mjs +1026 -0
- package/server/repositories/image-conversation-repository.mjs +317 -0
- package/server/repositories/message-repository.mjs +434 -0
- package/server/repositories/question-repository.mjs +161 -0
- package/server/repositories/session-repository.mjs +320 -0
- package/server/repositories/share-visibility-statements.mjs +24 -0
- package/server/routes/ask-user-routes.mjs +292 -0
- package/server/routes/cache-routes.mjs +44 -0
- package/server/routes/git-routes.mjs +92 -0
- package/server/routes/messages-routes-test-harness.mjs +111 -0
- package/server/routes/messages-routes.mjs +7107 -0
- package/server/routes/preview-routes.mjs +108 -0
- package/server/routes/push-routes.mjs +59 -0
- package/server/routes/relay-board-routes.mjs +268 -0
- package/server/routes/sessions-routes.mjs +6150 -0
- package/server/server-runtime.mjs +7463 -0
- package/server/server.js +24 -0
- package/server/services/ask-user-routing-service.mjs +117 -0
- package/server/services/cache-rebuild-service.mjs +236 -0
- package/server/services/claude-auth-service.mjs +578 -0
- package/server/services/claude-context-usage.mjs +240 -0
- package/server/services/claude-session-root-service.mjs +228 -0
- package/server/services/cli-install-service.mjs +1152 -0
- package/server/services/cli-process-runner.mjs +181 -0
- package/server/services/cloudflared-tunnel-service.mjs +358 -0
- package/server/services/context-snapshot-service.mjs +499 -0
- package/server/services/context-usage-view.mjs +160 -0
- package/server/services/conversation-preferences-service.mjs +208 -0
- package/server/services/cursor-dashboard-usage.mjs +217 -0
- package/server/services/cursor-session-root-service.mjs +62 -0
- package/server/services/delete-archive-service.mjs +154 -0
- package/server/services/drives-path-helpers.mjs +66 -0
- package/server/services/fixtures/claude-auth-stub.sh +62 -0
- package/server/services/fixtures/cli-install-stub.sh +75 -0
- package/server/services/fixtures/grok-stub.sh +86 -0
- package/server/services/generated-image-cleanup-service.mjs +114 -0
- package/server/services/git-changes-service.mjs +195 -0
- package/server/services/github-billing-usage.mjs +133 -0
- package/server/services/grok-auth-service.mjs +593 -0
- package/server/services/grok-billing-usage.mjs +120 -0
- package/server/services/image-context-service.mjs +106 -0
- package/server/services/image-operation-service.mjs +135 -0
- package/server/services/keyed-mutex-service.mjs +82 -0
- package/server/services/mime-sniffer.mjs +118 -0
- package/server/services/oar-cli-helpers.mjs +109 -0
- package/server/services/oar-state-migration-service.mjs +243 -0
- package/server/services/plan-usage-claude.mjs +387 -0
- package/server/services/plan-usage-contract.mjs +271 -0
- package/server/services/plan-usage-copilot.mjs +421 -0
- package/server/services/plan-usage-cursor.mjs +566 -0
- package/server/services/plan-usage-grok.mjs +436 -0
- package/server/services/plan-usage-service.mjs +487 -0
- package/server/services/preview-health-probe.mjs +82 -0
- package/server/services/preview-proxy-server.mjs +498 -0
- package/server/services/preview-registry-service.mjs +457 -0
- package/server/services/preview-static-handler.mjs +237 -0
- package/server/services/provider-model-selection.mjs +91 -0
- package/server/services/provider-reasoning-effort.mjs +94 -0
- package/server/services/push-active-device-service.mjs +57 -0
- package/server/services/push-dispatch-service.mjs +459 -0
- package/server/services/rate-limit-service.mjs +63 -0
- package/server/services/relay-bridge-owner-service.mjs +84 -0
- package/server/services/relay-cli-launcher-service.mjs +209 -0
- package/server/services/relay-prompt-sanitizer.mjs +137 -0
- package/server/services/relay-queue-gate-service.mjs +38 -0
- package/server/services/relay-restart-orchestrator-service.mjs +935 -0
- package/server/services/relay-singleton-guard.mjs +124 -0
- package/server/services/safe-served-content.mjs +57 -0
- package/server/services/sdk-session-import-service.mjs +271 -0
- package/server/services/sdk-session-sync-service.mjs +207 -0
- package/server/services/session-history-refresh-service.mjs +497 -0
- package/server/services/session-transcript-service.mjs +419 -0
- package/server/services/session-worker-launch-service.mjs +786 -0
- package/server/services/session-worker-process-service.mjs +319 -0
- package/server/services/session-worker-registry-service.mjs +194 -0
- package/server/services/session-worker-stop-service.mjs +141 -0
- package/server/services/session-worker-supervisor-service.mjs +898 -0
- package/server/services/session-worker-websocket-service.mjs +483 -0
- package/server/services/ssh-tunnel-manager-service.mjs +423 -0
- package/server/services/status-event-service.mjs +158 -0
- package/server/services/tmux-inspector-access-policy.mjs +77 -0
- package/server/services/tmux-inspector-socket-service.mjs +191 -0
- package/server/services/tmux-inspector-stream-service.mjs +552 -0
- package/server/services/tunnel-worker-path-guard.mjs +107 -0
- package/server/services/upload-sweep.mjs +60 -0
- package/server/services/usage-snapshot-helpers.mjs +209 -0
- package/server/services/windows-autostart-service.mjs +115 -0
- package/server/services/workspace-root-defaults-service.mjs +34 -0
- package/server/services/workspace-root-path-policy.mjs +196 -0
- package/server/services/workspace-root-relaunch-service.mjs +132 -0
- package/server/services/workspace-symlink-guard.mjs +54 -0
- package/server/tools/capture-proxy.mjs +173 -0
- package/server/tty-console-bootstrap.mjs +75 -0
- package/server/uploads/README.md +36 -0
- package/server/uploads/image_input.md +540 -0
- package/server/windows-terminal-launcher.mjs +29 -0
- package/server/workspace-root.mjs +87 -0
- package/shared/ask-user-bridge.mjs +131 -0
- package/shared/auto-compact-window.mjs +116 -0
- package/shared/background-task-timeout.mjs +52 -0
- package/shared/claude-thinking.mjs +101 -0
- package/shared/context-window-fallbacks.mjs +148 -0
- package/shared/control-poller.mjs +115 -0
- package/shared/empty-turn-completion.mjs +20 -0
- package/shared/model-catalog-freshness.mjs +11 -0
- package/shared/model-descriptors.mjs +118 -0
- package/shared/model-id.mjs +147 -0
- package/shared/model-refresh.mjs +24 -0
- package/shared/openai-reasoning.mjs +13 -0
- package/shared/plan-lines.mjs +13 -0
- package/shared/preview-instructions.mjs +77 -0
- package/shared/preview-tool-core.mjs +191 -0
- package/shared/provider-routing.mjs +29 -0
- package/shared/question-schema.mjs +315 -0
- package/shared/question-timeout.mjs +18 -0
- package/shared/stream-emit-gating.mjs +28 -0
- package/shared/subagent-run-id.mjs +17 -0
- package/shared/thought-cap.mjs +13 -0
- package/shared/tool-activity.mjs +98 -0
- package/shared/turn-ceiling.mjs +53 -0
- package/shared/worker-bootstrap.mjs +44 -0
- package/shared/worker-crash-guard.mjs +60 -0
|
@@ -0,0 +1,1484 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defaultSessionWorkspaceRootPath,
|
|
3
|
+
defaultSessionWorkspaceRootWarning,
|
|
4
|
+
serverPlatform,
|
|
5
|
+
showTransientRelayNotice,
|
|
6
|
+
} from './store.js';
|
|
7
|
+
import {
|
|
8
|
+
apiFetch,
|
|
9
|
+
loadClaudeSettings,
|
|
10
|
+
loadCopilotSettings,
|
|
11
|
+
updateCopilotSettings,
|
|
12
|
+
loadCursorSettings,
|
|
13
|
+
loadCursorAllowanceSettings,
|
|
14
|
+
updateCursorAllowanceSettings,
|
|
15
|
+
loadCursorDashboardTokenSettings,
|
|
16
|
+
updateCursorDashboardTokenSettings,
|
|
17
|
+
loadGrokAllowanceSettings,
|
|
18
|
+
updateGrokAllowanceSettings,
|
|
19
|
+
loadGrokSettings,
|
|
20
|
+
loadOpenAISettings,
|
|
21
|
+
updateClaudeSettings,
|
|
22
|
+
updateCursorSettings,
|
|
23
|
+
updateGrokSettings,
|
|
24
|
+
updateDefaultSessionWorkspaceRoot,
|
|
25
|
+
updateOpenAISettings,
|
|
26
|
+
loadWindowsAutostartSetting,
|
|
27
|
+
updateWindowsAutostartSetting,
|
|
28
|
+
loadTurnCeilingSetting,
|
|
29
|
+
updateTurnCeilingSetting as requestTurnCeilingSetting,
|
|
30
|
+
loadBackgroundTaskTimeoutSetting,
|
|
31
|
+
updateBackgroundTaskTimeoutSetting as requestBackgroundTaskTimeoutSetting,
|
|
32
|
+
} from './api-client.js';
|
|
33
|
+
import { syncFontScaleSelect } from './font-scaling.js';
|
|
34
|
+
import { syncPwaAppNameInput } from './pwa-install.js';
|
|
35
|
+
import { normalizeKnownCwdPath } from './cwd-picker.js';
|
|
36
|
+
import { refreshPushSettingsSection } from './push-settings.js';
|
|
37
|
+
import { selectSettingsTab } from './settings-tabs.js';
|
|
38
|
+
import { refreshClaudeAuthSection } from './claude-auth-ui.js';
|
|
39
|
+
import { refreshGrokAuthSection } from './grok-auth-ui.js';
|
|
40
|
+
import { refreshCliInstallSections } from './cli-install-ui.js';
|
|
41
|
+
import {
|
|
42
|
+
getPreviews,
|
|
43
|
+
renderPreviewRowsInto,
|
|
44
|
+
setPreviews,
|
|
45
|
+
subscribePreviews,
|
|
46
|
+
} from './preview-cards.mjs';
|
|
47
|
+
|
|
48
|
+
const THEME_STORAGE_KEY = 'copilot_theme';
|
|
49
|
+
const SHOW_SUSPEND_HOST_STORAGE_KEY = 'copilot_show_suspend_host';
|
|
50
|
+
|
|
51
|
+
let defaultSessionWorkspaceRootUpdateInFlight = false;
|
|
52
|
+
let openAISettingsUpdateInFlight = false;
|
|
53
|
+
let openAISettingsState = {
|
|
54
|
+
configured: false,
|
|
55
|
+
enabled: false,
|
|
56
|
+
model: 'gpt-4o',
|
|
57
|
+
baseUrl: 'https://api.openai.com/v1',
|
|
58
|
+
};
|
|
59
|
+
let openAISettingsInputsDirty = false;
|
|
60
|
+
|
|
61
|
+
function ensureOpenAISettingsInputTracking() {
|
|
62
|
+
for (const id of ['openai-api-key-input', 'openai-model-input', 'openai-base-url-input']) {
|
|
63
|
+
const input = document.getElementById(id);
|
|
64
|
+
if (!input || input.dataset.openaiDirtyTracking === '1') continue;
|
|
65
|
+
input.dataset.openaiDirtyTracking = '1';
|
|
66
|
+
input.addEventListener('input', () => {
|
|
67
|
+
openAISettingsInputsDirty = true;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
let windowsAutostartUpdateInFlight = false;
|
|
72
|
+
let windowsAutostartEnabled = false;
|
|
73
|
+
|
|
74
|
+
// Mirrors shared/turn-ceiling.mjs, which the browser cannot import — only
|
|
75
|
+
// server/public is served. The authoritative bounds come from the API response;
|
|
76
|
+
// these are just the pre-response defaults.
|
|
77
|
+
let turnCeilingMinutes = 60;
|
|
78
|
+
let turnCeilingUpdateInFlight = false;
|
|
79
|
+
|
|
80
|
+
function formatTurnCeilingLabel(minutes) {
|
|
81
|
+
const value = Math.max(0, Math.round(Number(minutes) || 0));
|
|
82
|
+
if (value === 0) return 'No limit';
|
|
83
|
+
if (value < 60) return `${value} min`;
|
|
84
|
+
const hours = Math.floor(value / 60);
|
|
85
|
+
const remainder = value % 60;
|
|
86
|
+
return remainder ? `${hours} h ${remainder} min` : `${hours} h`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function syncTurnCeilingSlider() {
|
|
90
|
+
const slider = document.getElementById('turn-ceiling-slider');
|
|
91
|
+
const label = document.getElementById('turn-ceiling-value');
|
|
92
|
+
if (slider instanceof HTMLInputElement) {
|
|
93
|
+
slider.value = String(turnCeilingMinutes);
|
|
94
|
+
slider.disabled = turnCeilingUpdateInFlight;
|
|
95
|
+
}
|
|
96
|
+
if (label) label.textContent = formatTurnCeilingLabel(turnCeilingMinutes);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Live label feedback while dragging; nothing is persisted until change fires. */
|
|
100
|
+
export function previewTurnCeilingSetting(value) {
|
|
101
|
+
const label = document.getElementById('turn-ceiling-value');
|
|
102
|
+
if (label) label.textContent = formatTurnCeilingLabel(value);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function refreshTurnCeilingSetting() {
|
|
106
|
+
syncTurnCeilingSlider();
|
|
107
|
+
try {
|
|
108
|
+
const result = await loadTurnCeilingSetting();
|
|
109
|
+
if (!result || !Number.isFinite(Number(result.ceilingMinutes))) return;
|
|
110
|
+
turnCeilingMinutes = Number(result.ceilingMinutes);
|
|
111
|
+
const slider = document.getElementById('turn-ceiling-slider');
|
|
112
|
+
if (slider instanceof HTMLInputElement) {
|
|
113
|
+
if (Number.isFinite(Number(result.minMinutes))) slider.min = String(result.minMinutes);
|
|
114
|
+
if (Number.isFinite(Number(result.maxMinutes))) slider.max = String(result.maxMinutes);
|
|
115
|
+
if (Number.isFinite(Number(result.stepMinutes))) slider.step = String(result.stepMinutes);
|
|
116
|
+
}
|
|
117
|
+
} catch {
|
|
118
|
+
// Leave the slider at its last known value; the setting is not critical.
|
|
119
|
+
} finally {
|
|
120
|
+
syncTurnCeilingSlider();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function updateTurnCeilingSetting(value) {
|
|
125
|
+
if (turnCeilingUpdateInFlight) {
|
|
126
|
+
syncTurnCeilingSlider();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const requested = Math.max(0, Math.round(Number(value) || 0));
|
|
130
|
+
turnCeilingUpdateInFlight = true;
|
|
131
|
+
syncTurnCeilingSlider();
|
|
132
|
+
try {
|
|
133
|
+
const result = await requestTurnCeilingSetting(requested);
|
|
134
|
+
turnCeilingMinutes = Number.isFinite(Number(result?.ceilingMinutes))
|
|
135
|
+
? Number(result.ceilingMinutes)
|
|
136
|
+
: requested;
|
|
137
|
+
showTransientRelayNotice(
|
|
138
|
+
turnCeilingMinutes === 0
|
|
139
|
+
? 'Turns will run without a time limit.'
|
|
140
|
+
: `Max turn duration set to ${formatTurnCeilingLabel(turnCeilingMinutes)}.`,
|
|
141
|
+
);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
alert(error?.message || 'Failed to update the max turn duration.');
|
|
144
|
+
} finally {
|
|
145
|
+
turnCeilingUpdateInFlight = false;
|
|
146
|
+
syncTurnCeilingSlider();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Mirrors shared/background-task-timeout.mjs (the browser cannot import it).
|
|
151
|
+
// Bounds come from the API response; these are just pre-response defaults.
|
|
152
|
+
let backgroundTaskTimeoutMinutes = 0;
|
|
153
|
+
let backgroundTaskTimeoutUpdateInFlight = false;
|
|
154
|
+
|
|
155
|
+
function syncBackgroundTaskTimeoutSlider() {
|
|
156
|
+
const slider = document.getElementById('background-task-timeout-slider');
|
|
157
|
+
const label = document.getElementById('background-task-timeout-value');
|
|
158
|
+
if (slider instanceof HTMLInputElement) {
|
|
159
|
+
slider.value = String(backgroundTaskTimeoutMinutes);
|
|
160
|
+
slider.disabled = backgroundTaskTimeoutUpdateInFlight;
|
|
161
|
+
}
|
|
162
|
+
if (label) label.textContent = formatTurnCeilingLabel(backgroundTaskTimeoutMinutes);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Live label feedback while dragging; nothing is persisted until change fires. */
|
|
166
|
+
export function previewBackgroundTaskTimeoutSetting(value) {
|
|
167
|
+
const label = document.getElementById('background-task-timeout-value');
|
|
168
|
+
if (label) label.textContent = formatTurnCeilingLabel(value);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function refreshBackgroundTaskTimeoutSetting() {
|
|
172
|
+
syncBackgroundTaskTimeoutSlider();
|
|
173
|
+
try {
|
|
174
|
+
const result = await loadBackgroundTaskTimeoutSetting();
|
|
175
|
+
if (!result || !Number.isFinite(Number(result.timeoutMinutes))) return;
|
|
176
|
+
backgroundTaskTimeoutMinutes = Number(result.timeoutMinutes);
|
|
177
|
+
const slider = document.getElementById('background-task-timeout-slider');
|
|
178
|
+
if (slider instanceof HTMLInputElement) {
|
|
179
|
+
if (Number.isFinite(Number(result.minMinutes))) slider.min = String(result.minMinutes);
|
|
180
|
+
if (Number.isFinite(Number(result.maxMinutes))) slider.max = String(result.maxMinutes);
|
|
181
|
+
if (Number.isFinite(Number(result.stepMinutes))) slider.step = String(result.stepMinutes);
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
// Leave the slider at its last known value; the setting is not critical.
|
|
185
|
+
} finally {
|
|
186
|
+
syncBackgroundTaskTimeoutSlider();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function updateBackgroundTaskTimeoutSetting(value) {
|
|
191
|
+
if (backgroundTaskTimeoutUpdateInFlight) {
|
|
192
|
+
syncBackgroundTaskTimeoutSlider();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const requested = Math.max(0, Math.round(Number(value) || 0));
|
|
196
|
+
backgroundTaskTimeoutUpdateInFlight = true;
|
|
197
|
+
syncBackgroundTaskTimeoutSlider();
|
|
198
|
+
try {
|
|
199
|
+
const result = await requestBackgroundTaskTimeoutSetting(requested);
|
|
200
|
+
backgroundTaskTimeoutMinutes = Number.isFinite(Number(result?.timeoutMinutes))
|
|
201
|
+
? Number(result.timeoutMinutes)
|
|
202
|
+
: requested;
|
|
203
|
+
showTransientRelayNotice(
|
|
204
|
+
backgroundTaskTimeoutMinutes === 0
|
|
205
|
+
? 'Background tasks will run without a time limit.'
|
|
206
|
+
: `Background task timeout set to ${formatTurnCeilingLabel(backgroundTaskTimeoutMinutes)}.`,
|
|
207
|
+
);
|
|
208
|
+
} catch (error) {
|
|
209
|
+
alert(error?.message || 'Failed to update the background task timeout.');
|
|
210
|
+
} finally {
|
|
211
|
+
backgroundTaskTimeoutUpdateInFlight = false;
|
|
212
|
+
syncBackgroundTaskTimeoutSlider();
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function readLocalStorage(key) {
|
|
217
|
+
try {
|
|
218
|
+
return localStorage.getItem(key);
|
|
219
|
+
} catch {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function writeLocalStorage(key, value) {
|
|
225
|
+
try {
|
|
226
|
+
localStorage.setItem(key, value);
|
|
227
|
+
return true;
|
|
228
|
+
} catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function closeChatActionsMenu() {
|
|
234
|
+
const menu = document.getElementById('chat-actions-menu');
|
|
235
|
+
const trigger = document.getElementById('chat-actions-menu-btn');
|
|
236
|
+
const backdrop = document.getElementById('chat-actions-menu-backdrop');
|
|
237
|
+
if (menu) menu.hidden = true;
|
|
238
|
+
if (trigger) trigger.setAttribute('aria-expanded', 'false');
|
|
239
|
+
if (backdrop && !window.__chatActionsMenuShieldTimer) backdrop.classList.remove('visible');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function syncDefaultSessionWorkspaceRootInput() {
|
|
243
|
+
const input = document.getElementById('default-session-workspace-root-input');
|
|
244
|
+
if (!input) return;
|
|
245
|
+
input.value = normalizeKnownCwdPath(defaultSessionWorkspaceRootPath || '');
|
|
246
|
+
if (defaultSessionWorkspaceRootWarning) {
|
|
247
|
+
input.title = defaultSessionWorkspaceRootWarning;
|
|
248
|
+
} else {
|
|
249
|
+
input.removeAttribute('title');
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function setOpenAISettingsControlsDisabled(disabled) {
|
|
254
|
+
for (const id of [
|
|
255
|
+
'openai-api-key-input',
|
|
256
|
+
'openai-model-input',
|
|
257
|
+
'openai-base-url-input',
|
|
258
|
+
'openai-enabled-toggle',
|
|
259
|
+
'openai-save-btn',
|
|
260
|
+
'openai-remove-btn',
|
|
261
|
+
]) {
|
|
262
|
+
const element = document.getElementById(id);
|
|
263
|
+
if (element) element.disabled = disabled;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function applyOpenAISettingsState(settings = {}, { resetInputs = false } = {}) {
|
|
268
|
+
ensureOpenAISettingsInputTracking();
|
|
269
|
+
openAISettingsState = {
|
|
270
|
+
configured: settings?.configured === true,
|
|
271
|
+
enabled: settings?.configured === true && settings?.enabled === true,
|
|
272
|
+
model: String(settings?.model || openAISettingsState.model || 'gpt-4o').trim() || 'gpt-4o',
|
|
273
|
+
baseUrl: String(settings?.baseUrl || openAISettingsState.baseUrl || 'https://api.openai.com/v1').trim() || 'https://api.openai.com/v1',
|
|
274
|
+
};
|
|
275
|
+
const keyInput = document.getElementById('openai-api-key-input');
|
|
276
|
+
const modelInput = document.getElementById('openai-model-input');
|
|
277
|
+
const baseUrlInput = document.getElementById('openai-base-url-input');
|
|
278
|
+
const toggle = document.getElementById('openai-enabled-toggle');
|
|
279
|
+
const removeButton = document.getElementById('openai-remove-btn');
|
|
280
|
+
const status = document.getElementById('openai-settings-status');
|
|
281
|
+
if (keyInput && (!openAISettingsInputsDirty || resetInputs)) {
|
|
282
|
+
keyInput.value = '';
|
|
283
|
+
keyInput.placeholder = openAISettingsState.configured ? 'Saved API key (enter to replace)' : 'sk-...';
|
|
284
|
+
}
|
|
285
|
+
if (modelInput && (!openAISettingsInputsDirty || resetInputs)) modelInput.value = openAISettingsState.model;
|
|
286
|
+
if (baseUrlInput && (!openAISettingsInputsDirty || resetInputs)) {
|
|
287
|
+
baseUrlInput.value = openAISettingsState.baseUrl === 'https://api.openai.com/v1' ? '' : openAISettingsState.baseUrl;
|
|
288
|
+
baseUrlInput.placeholder = 'https://api.openai.com/v1';
|
|
289
|
+
}
|
|
290
|
+
if (resetInputs) openAISettingsInputsDirty = false;
|
|
291
|
+
if (toggle) {
|
|
292
|
+
toggle.checked = openAISettingsState.enabled;
|
|
293
|
+
toggle.disabled = openAISettingsUpdateInFlight || !openAISettingsState.configured;
|
|
294
|
+
}
|
|
295
|
+
if (removeButton) {
|
|
296
|
+
removeButton.disabled = openAISettingsUpdateInFlight || !openAISettingsState.configured;
|
|
297
|
+
}
|
|
298
|
+
if (status) {
|
|
299
|
+
status.textContent = openAISettingsState.enabled
|
|
300
|
+
? `OpenAI API is enabled. Select OpenAI in New Chat to use model ${openAISettingsState.model}.`
|
|
301
|
+
: (openAISettingsState.configured
|
|
302
|
+
? 'API key saved but currently disabled. Enable it to allow OpenAI selection in New Chat.'
|
|
303
|
+
: 'Not configured. New conversations use GitHub Copilot.');
|
|
304
|
+
status.dataset.state = openAISettingsState.enabled
|
|
305
|
+
? 'active'
|
|
306
|
+
: (openAISettingsState.configured ? 'saved' : 'unconfigured');
|
|
307
|
+
}
|
|
308
|
+
window.syncAutoModelAvailability?.();
|
|
309
|
+
return openAISettingsState;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export async function refreshOpenAISettingsState() {
|
|
313
|
+
const settings = await loadOpenAISettings();
|
|
314
|
+
if (!settings) return null;
|
|
315
|
+
return applyOpenAISettingsState(settings);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Copilot has no key and no model list in settings — it authenticates through
|
|
319
|
+
// the host's `copilot` login and its catalog comes from the CLI — so the only
|
|
320
|
+
// thing this panel owns is which engine runs Copilot conversations.
|
|
321
|
+
let copilotSettingsUpdateInFlight = false;
|
|
322
|
+
let copilotSettingsState = { engine: 'extension' };
|
|
323
|
+
// The reason the last save was refused, shown in place of the engine
|
|
324
|
+
// description. Every apply sets it — so a later success, a refresh, or a
|
|
325
|
+
// broadcast from another tab all clear it by simply not passing one.
|
|
326
|
+
let copilotSettingsSaveError = '';
|
|
327
|
+
|
|
328
|
+
function setCopilotSettingsControlsDisabled(disabled) {
|
|
329
|
+
for (const id of ['copilot-engine-select', 'copilot-save-btn']) {
|
|
330
|
+
const element = document.getElementById(id);
|
|
331
|
+
if (element) element.disabled = disabled;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function applyCopilotSettingsState(settings = {}, { error = '' } = {}) {
|
|
336
|
+
copilotSettingsSaveError = String(error || '');
|
|
337
|
+
const engine = String(settings?.engine || copilotSettingsState.engine || 'extension').trim().toLowerCase();
|
|
338
|
+
copilotSettingsState = { engine: engine === 'sdk' ? 'sdk' : 'extension' };
|
|
339
|
+
const select = document.getElementById('copilot-engine-select');
|
|
340
|
+
const status = document.getElementById('copilot-settings-status');
|
|
341
|
+
// A select has no free text to lose, so unlike the Claude model input there
|
|
342
|
+
// is no dirty-tracking here: a socket refresh may always overwrite it — but
|
|
343
|
+
// never while the user's own save is still in flight.
|
|
344
|
+
if (select && !copilotSettingsUpdateInFlight) select.value = copilotSettingsState.engine;
|
|
345
|
+
if (status) {
|
|
346
|
+
if (copilotSettingsSaveError) {
|
|
347
|
+
status.textContent = copilotSettingsSaveError;
|
|
348
|
+
status.dataset.state = 'error';
|
|
349
|
+
} else {
|
|
350
|
+
status.textContent = copilotSettingsState.engine === 'sdk'
|
|
351
|
+
? 'New Copilot conversations run on the experimental headless SDK worker. No tmux inspector for these sessions.'
|
|
352
|
+
: 'New Copilot conversations run on the Copilot CLI with the web-relay extension (current default).';
|
|
353
|
+
status.dataset.state = copilotSettingsState.engine === 'sdk' ? 'active' : 'unconfigured';
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return copilotSettingsState;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export async function refreshCopilotSettingsState() {
|
|
360
|
+
const settings = await loadCopilotSettings();
|
|
361
|
+
if (!settings) return null;
|
|
362
|
+
return applyCopilotSettingsState(settings);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
async function syncCopilotSettingsInputs() {
|
|
366
|
+
const status = document.getElementById('copilot-settings-status');
|
|
367
|
+
if (!status) return;
|
|
368
|
+
const settings = await refreshCopilotSettingsState();
|
|
369
|
+
if (!settings) {
|
|
370
|
+
status.textContent = 'Unable to load Copilot settings.';
|
|
371
|
+
status.dataset.state = 'error';
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export async function saveCopilotSettings() {
|
|
376
|
+
if (copilotSettingsUpdateInFlight) return;
|
|
377
|
+
const select = document.getElementById('copilot-engine-select');
|
|
378
|
+
const engine = String(select?.value || '').trim().toLowerCase() || 'extension';
|
|
379
|
+
copilotSettingsUpdateInFlight = true;
|
|
380
|
+
setCopilotSettingsControlsDisabled(true);
|
|
381
|
+
// What to render once the save settles: the server's answer when it accepted,
|
|
382
|
+
// the last known state plus a reason when it did not. Assigned in the body,
|
|
383
|
+
// applied ONCE in the `finally` — clearing the in-flight flag early just to
|
|
384
|
+
// apply mid-try, then applying again on the way out, made the success path
|
|
385
|
+
// render twice and made the flag's meaning depend on which branch you were in.
|
|
386
|
+
let settled = copilotSettingsState;
|
|
387
|
+
let saveError = '';
|
|
388
|
+
try {
|
|
389
|
+
const result = await updateCopilotSettings({ engine });
|
|
390
|
+
if (!result) throw new Error('Failed to save Copilot settings.');
|
|
391
|
+
settled = result;
|
|
392
|
+
showTransientRelayNotice(
|
|
393
|
+
String(result.engine) === 'sdk'
|
|
394
|
+
? 'Copilot engine set to SDK (experimental). New conversations use the headless worker; running sessions keep the extension until they restart.'
|
|
395
|
+
: 'Copilot engine set to Extension. New conversations use the Copilot CLI.',
|
|
396
|
+
6000,
|
|
397
|
+
);
|
|
398
|
+
} catch (error) {
|
|
399
|
+
// The relay refuses the SDK engine with a specific, actionable reason (no
|
|
400
|
+
// resolved SDK path; session-worker routing disabled). It goes in the
|
|
401
|
+
// status line as well as the alert, because the alert is gone the moment it
|
|
402
|
+
// is dismissed and the panel would otherwise show the old engine with no
|
|
403
|
+
// explanation of why the choice did not stick.
|
|
404
|
+
saveError = String(error?.message || 'Failed to save Copilot settings.');
|
|
405
|
+
alert(saveError);
|
|
406
|
+
} finally {
|
|
407
|
+
copilotSettingsUpdateInFlight = false;
|
|
408
|
+
setCopilotSettingsControlsDisabled(false);
|
|
409
|
+
applyCopilotSettingsState(settled, { error: saveError });
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let claudeSettingsUpdateInFlight = false;
|
|
414
|
+
let claudeSettingsState = {
|
|
415
|
+
configured: false,
|
|
416
|
+
enabled: false,
|
|
417
|
+
model: 'claude-sonnet-5',
|
|
418
|
+
models: [],
|
|
419
|
+
};
|
|
420
|
+
let claudeSettingsInputsDirty = false;
|
|
421
|
+
|
|
422
|
+
function ensureClaudeSettingsInputTracking() {
|
|
423
|
+
const input = document.getElementById('claude-model-input');
|
|
424
|
+
if (!input || input.dataset.claudeDirtyTracking === '1') return;
|
|
425
|
+
input.dataset.claudeDirtyTracking = '1';
|
|
426
|
+
input.addEventListener('input', () => {
|
|
427
|
+
claudeSettingsInputsDirty = true;
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function setClaudeSettingsControlsDisabled(disabled) {
|
|
432
|
+
for (const id of ['claude-model-input', 'claude-enabled-toggle', 'claude-save-btn']) {
|
|
433
|
+
const element = document.getElementById(id);
|
|
434
|
+
if (element) element.disabled = disabled;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
export function applyClaudeSettingsState(settings = {}, { resetInputs = false } = {}) {
|
|
439
|
+
ensureClaudeSettingsInputTracking();
|
|
440
|
+
claudeSettingsState = {
|
|
441
|
+
configured: settings?.enabled === true,
|
|
442
|
+
enabled: settings?.enabled === true,
|
|
443
|
+
model: String(settings?.model || claudeSettingsState.model || 'claude-sonnet-5').trim() || 'claude-sonnet-5',
|
|
444
|
+
models: Array.isArray(settings?.models) ? settings.models : claudeSettingsState.models,
|
|
445
|
+
};
|
|
446
|
+
const modelInput = document.getElementById('claude-model-input');
|
|
447
|
+
const toggle = document.getElementById('claude-enabled-toggle');
|
|
448
|
+
const status = document.getElementById('claude-settings-status');
|
|
449
|
+
if (modelInput && (!claudeSettingsInputsDirty || resetInputs)) modelInput.value = claudeSettingsState.model;
|
|
450
|
+
if (resetInputs) claudeSettingsInputsDirty = false;
|
|
451
|
+
if (toggle) {
|
|
452
|
+
toggle.checked = claudeSettingsState.enabled;
|
|
453
|
+
toggle.disabled = claudeSettingsUpdateInFlight;
|
|
454
|
+
}
|
|
455
|
+
if (status) {
|
|
456
|
+
status.textContent = claudeSettingsState.enabled
|
|
457
|
+
? `Claude is enabled. Select Claude in New Chat to use model ${claudeSettingsState.model}. Uses the host machine's logged-in Claude credentials.`
|
|
458
|
+
: 'Not enabled. Enable to allow Claude selection in New Chat (requires a logged-in Claude Code CLI on the relay host).';
|
|
459
|
+
status.dataset.state = claudeSettingsState.enabled ? 'active' : 'unconfigured';
|
|
460
|
+
}
|
|
461
|
+
window.syncAutoModelAvailability?.();
|
|
462
|
+
return claudeSettingsState;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export async function refreshClaudeSettingsState() {
|
|
466
|
+
const settings = await loadClaudeSettings();
|
|
467
|
+
if (!settings) return null;
|
|
468
|
+
return applyClaudeSettingsState(settings);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
async function syncClaudeSettingsInputs() {
|
|
472
|
+
const status = document.getElementById('claude-settings-status');
|
|
473
|
+
if (!status) return;
|
|
474
|
+
const settings = await refreshClaudeSettingsState();
|
|
475
|
+
if (!settings) {
|
|
476
|
+
status.textContent = 'Unable to load Claude settings.';
|
|
477
|
+
status.dataset.state = 'error';
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export async function saveClaudeSettings() {
|
|
482
|
+
if (claudeSettingsUpdateInFlight) return;
|
|
483
|
+
const modelInput = document.getElementById('claude-model-input');
|
|
484
|
+
const model = String(modelInput?.value || '').trim() || 'claude-sonnet-5';
|
|
485
|
+
claudeSettingsUpdateInFlight = true;
|
|
486
|
+
setClaudeSettingsControlsDisabled(true);
|
|
487
|
+
try {
|
|
488
|
+
const result = await updateClaudeSettings({ model });
|
|
489
|
+
if (!result) throw new Error('Failed to save Claude settings.');
|
|
490
|
+
applyClaudeSettingsState(result, { resetInputs: true });
|
|
491
|
+
showTransientRelayNotice(
|
|
492
|
+
result.warning
|
|
493
|
+
? `Claude settings saved. ${result.warning}`
|
|
494
|
+
: `Claude settings saved for ${result.model}.`,
|
|
495
|
+
result.warning ? 8000 : 4000,
|
|
496
|
+
);
|
|
497
|
+
} catch (error) {
|
|
498
|
+
alert(error?.message || 'Failed to save Claude settings.');
|
|
499
|
+
} finally {
|
|
500
|
+
claudeSettingsUpdateInFlight = false;
|
|
501
|
+
setClaudeSettingsControlsDisabled(false);
|
|
502
|
+
applyClaudeSettingsState(claudeSettingsState);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export async function toggleClaudeProvider(enabled) {
|
|
507
|
+
if (claudeSettingsUpdateInFlight) return;
|
|
508
|
+
claudeSettingsUpdateInFlight = true;
|
|
509
|
+
setClaudeSettingsControlsDisabled(true);
|
|
510
|
+
try {
|
|
511
|
+
const result = await updateClaudeSettings({ enabled: enabled === true });
|
|
512
|
+
if (!result) throw new Error('Failed to update the Claude provider.');
|
|
513
|
+
applyClaudeSettingsState(result);
|
|
514
|
+
const providerLabel = result.enabled ? 'Claude provider enabled' : 'Claude provider disabled';
|
|
515
|
+
showTransientRelayNotice(
|
|
516
|
+
`${providerLabel}.${result.warning ? ` ${result.warning}` : ''}`,
|
|
517
|
+
result.warning ? 8000 : 4500,
|
|
518
|
+
);
|
|
519
|
+
} catch (error) {
|
|
520
|
+
applyClaudeSettingsState(claudeSettingsState);
|
|
521
|
+
alert(error?.message || 'Failed to update the Claude provider.');
|
|
522
|
+
} finally {
|
|
523
|
+
claudeSettingsUpdateInFlight = false;
|
|
524
|
+
setClaudeSettingsControlsDisabled(false);
|
|
525
|
+
applyClaudeSettingsState(claudeSettingsState);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
let grokSettingsUpdateInFlight = false;
|
|
530
|
+
let grokSettingsState = {
|
|
531
|
+
configured: false,
|
|
532
|
+
enabled: false,
|
|
533
|
+
model: 'grok-4.5',
|
|
534
|
+
models: [],
|
|
535
|
+
};
|
|
536
|
+
let grokSettingsInputsDirty = false;
|
|
537
|
+
|
|
538
|
+
function ensureGrokSettingsInputTracking() {
|
|
539
|
+
const input = document.getElementById('grok-model-input');
|
|
540
|
+
if (!input || input.dataset.grokDirtyTracking === '1') return;
|
|
541
|
+
input.dataset.grokDirtyTracking = '1';
|
|
542
|
+
input.addEventListener('input', () => {
|
|
543
|
+
grokSettingsInputsDirty = true;
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function setGrokSettingsControlsDisabled(disabled) {
|
|
548
|
+
for (const id of ['grok-model-input', 'grok-enabled-toggle', 'grok-save-btn']) {
|
|
549
|
+
const element = document.getElementById(id);
|
|
550
|
+
if (element) element.disabled = disabled;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
export function applyGrokSettingsState(settings = {}, { resetInputs = false } = {}) {
|
|
555
|
+
ensureGrokSettingsInputTracking();
|
|
556
|
+
grokSettingsState = {
|
|
557
|
+
configured: settings?.enabled === true,
|
|
558
|
+
enabled: settings?.enabled === true,
|
|
559
|
+
model: String(settings?.model || grokSettingsState.model || 'grok-4.5').trim() || 'grok-4.5',
|
|
560
|
+
models: Array.isArray(settings?.models) ? settings.models : grokSettingsState.models,
|
|
561
|
+
};
|
|
562
|
+
const modelInput = document.getElementById('grok-model-input');
|
|
563
|
+
const toggle = document.getElementById('grok-enabled-toggle');
|
|
564
|
+
const status = document.getElementById('grok-settings-status');
|
|
565
|
+
if (modelInput && (!grokSettingsInputsDirty || resetInputs)) modelInput.value = grokSettingsState.model;
|
|
566
|
+
if (resetInputs) grokSettingsInputsDirty = false;
|
|
567
|
+
if (toggle) {
|
|
568
|
+
toggle.checked = grokSettingsState.enabled;
|
|
569
|
+
toggle.disabled = grokSettingsUpdateInFlight;
|
|
570
|
+
}
|
|
571
|
+
if (status) {
|
|
572
|
+
status.textContent = grokSettingsState.enabled
|
|
573
|
+
? `Grok is enabled. Select Grok in New Chat to use model ${grokSettingsState.model}. Uses the host machine's logged-in Grok credentials (grok login or XAI_API_KEY).`
|
|
574
|
+
: 'Not enabled. Enable to allow Grok selection in New Chat (requires host Grok login or XAI_API_KEY).';
|
|
575
|
+
status.dataset.state = grokSettingsState.enabled ? 'active' : 'unconfigured';
|
|
576
|
+
}
|
|
577
|
+
window.syncAutoModelAvailability?.();
|
|
578
|
+
return grokSettingsState;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export async function refreshGrokSettingsState() {
|
|
582
|
+
const settings = await loadGrokSettings();
|
|
583
|
+
if (!settings) return null;
|
|
584
|
+
return applyGrokSettingsState(settings);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
async function syncGrokSettingsInputs() {
|
|
588
|
+
const status = document.getElementById('grok-settings-status');
|
|
589
|
+
if (!status) return;
|
|
590
|
+
const settings = await refreshGrokSettingsState();
|
|
591
|
+
if (!settings) {
|
|
592
|
+
status.textContent = 'Unable to load Grok settings.';
|
|
593
|
+
status.dataset.state = 'error';
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
export async function saveGrokSettings() {
|
|
598
|
+
if (grokSettingsUpdateInFlight) return;
|
|
599
|
+
const modelInput = document.getElementById('grok-model-input');
|
|
600
|
+
const model = String(modelInput?.value || '').trim() || 'grok-4.5';
|
|
601
|
+
grokSettingsUpdateInFlight = true;
|
|
602
|
+
setGrokSettingsControlsDisabled(true);
|
|
603
|
+
try {
|
|
604
|
+
const result = await updateGrokSettings({ model });
|
|
605
|
+
if (!result) throw new Error('Failed to save Grok settings.');
|
|
606
|
+
applyGrokSettingsState(result, { resetInputs: true });
|
|
607
|
+
showTransientRelayNotice(
|
|
608
|
+
result.warning
|
|
609
|
+
? `Grok settings saved. ${result.warning}`
|
|
610
|
+
: `Grok settings saved for ${result.model}.`,
|
|
611
|
+
result.warning ? 8000 : 4000,
|
|
612
|
+
);
|
|
613
|
+
} catch (error) {
|
|
614
|
+
alert(error?.message || 'Failed to save Grok settings.');
|
|
615
|
+
} finally {
|
|
616
|
+
grokSettingsUpdateInFlight = false;
|
|
617
|
+
setGrokSettingsControlsDisabled(false);
|
|
618
|
+
applyGrokSettingsState(grokSettingsState);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
export async function toggleGrokProvider(enabled) {
|
|
623
|
+
if (grokSettingsUpdateInFlight) return;
|
|
624
|
+
grokSettingsUpdateInFlight = true;
|
|
625
|
+
setGrokSettingsControlsDisabled(true);
|
|
626
|
+
try {
|
|
627
|
+
const result = await updateGrokSettings({ enabled: enabled === true });
|
|
628
|
+
if (!result) throw new Error('Failed to update the Grok provider.');
|
|
629
|
+
applyGrokSettingsState(result);
|
|
630
|
+
const providerLabel = result.enabled ? 'Grok provider enabled' : 'Grok provider disabled';
|
|
631
|
+
showTransientRelayNotice(
|
|
632
|
+
`${providerLabel}.${result.warning ? ` ${result.warning}` : ''}`,
|
|
633
|
+
result.warning ? 8000 : 4500,
|
|
634
|
+
);
|
|
635
|
+
} catch (error) {
|
|
636
|
+
applyGrokSettingsState(grokSettingsState);
|
|
637
|
+
alert(error?.message || 'Failed to update the Grok provider.');
|
|
638
|
+
} finally {
|
|
639
|
+
grokSettingsUpdateInFlight = false;
|
|
640
|
+
setGrokSettingsControlsDisabled(false);
|
|
641
|
+
applyGrokSettingsState(grokSettingsState);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
let cursorSettingsUpdateInFlight = false;
|
|
646
|
+
let cursorSettingsState = {
|
|
647
|
+
configured: false,
|
|
648
|
+
enabled: false,
|
|
649
|
+
model: 'composer-2.5',
|
|
650
|
+
};
|
|
651
|
+
let cursorSettingsInputsDirty = false;
|
|
652
|
+
|
|
653
|
+
function ensureCursorSettingsInputTracking() {
|
|
654
|
+
for (const id of ['cursor-api-key-input', 'cursor-model-input']) {
|
|
655
|
+
const input = document.getElementById(id);
|
|
656
|
+
if (!input || input.dataset.cursorDirtyTracking === '1') continue;
|
|
657
|
+
input.dataset.cursorDirtyTracking = '1';
|
|
658
|
+
input.addEventListener('input', () => {
|
|
659
|
+
cursorSettingsInputsDirty = true;
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function setCursorSettingsControlsDisabled(disabled) {
|
|
665
|
+
for (const id of [
|
|
666
|
+
'cursor-api-key-input',
|
|
667
|
+
'cursor-model-input',
|
|
668
|
+
'cursor-enabled-toggle',
|
|
669
|
+
'cursor-save-btn',
|
|
670
|
+
'cursor-remove-btn',
|
|
671
|
+
]) {
|
|
672
|
+
const element = document.getElementById(id);
|
|
673
|
+
if (element) element.disabled = disabled;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
export function applyCursorSettingsState(settings = {}, { resetInputs = false } = {}) {
|
|
678
|
+
ensureCursorSettingsInputTracking();
|
|
679
|
+
cursorSettingsState = {
|
|
680
|
+
configured: settings?.configured === true,
|
|
681
|
+
enabled: settings?.configured === true && settings?.enabled === true,
|
|
682
|
+
model: String(settings?.model || cursorSettingsState.model || 'composer-2.5').trim() || 'composer-2.5',
|
|
683
|
+
};
|
|
684
|
+
const keyInput = document.getElementById('cursor-api-key-input');
|
|
685
|
+
const modelInput = document.getElementById('cursor-model-input');
|
|
686
|
+
const toggle = document.getElementById('cursor-enabled-toggle');
|
|
687
|
+
const removeButton = document.getElementById('cursor-remove-btn');
|
|
688
|
+
const status = document.getElementById('cursor-settings-status');
|
|
689
|
+
if (keyInput && (!cursorSettingsInputsDirty || resetInputs)) {
|
|
690
|
+
keyInput.value = '';
|
|
691
|
+
keyInput.placeholder = cursorSettingsState.configured ? 'Saved API key (enter to replace)' : 'Cursor API key…';
|
|
692
|
+
}
|
|
693
|
+
if (modelInput && (!cursorSettingsInputsDirty || resetInputs)) modelInput.value = cursorSettingsState.model;
|
|
694
|
+
if (resetInputs) cursorSettingsInputsDirty = false;
|
|
695
|
+
if (toggle) {
|
|
696
|
+
toggle.checked = cursorSettingsState.enabled;
|
|
697
|
+
toggle.disabled = cursorSettingsUpdateInFlight || !cursorSettingsState.configured;
|
|
698
|
+
}
|
|
699
|
+
if (removeButton) {
|
|
700
|
+
removeButton.disabled = cursorSettingsUpdateInFlight || !cursorSettingsState.configured;
|
|
701
|
+
}
|
|
702
|
+
if (status) {
|
|
703
|
+
status.textContent = cursorSettingsState.enabled
|
|
704
|
+
? 'Cursor is enabled. Select Cursor in New Chat to use it.'
|
|
705
|
+
: (cursorSettingsState.configured
|
|
706
|
+
? 'API key saved but currently disabled. Enable it to allow Cursor selection in New Chat.'
|
|
707
|
+
: 'Not configured. New conversations use GitHub Copilot.');
|
|
708
|
+
status.dataset.state = cursorSettingsState.enabled
|
|
709
|
+
? 'active'
|
|
710
|
+
: (cursorSettingsState.configured ? 'saved' : 'unconfigured');
|
|
711
|
+
}
|
|
712
|
+
window.syncAutoModelAvailability?.();
|
|
713
|
+
return cursorSettingsState;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export async function refreshCursorSettingsState() {
|
|
717
|
+
const settings = await loadCursorSettings();
|
|
718
|
+
if (!settings) return null;
|
|
719
|
+
return applyCursorSettingsState(settings);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// ── Cursor manual plan allowances ──
|
|
723
|
+
// Cursor's SDK reports spend but not the included-pool balance, so these
|
|
724
|
+
// user-entered numbers are what the plan-usage card measures spend against.
|
|
725
|
+
let cursorAllowanceUpdateInFlight = false;
|
|
726
|
+
|
|
727
|
+
function setCursorAllowanceControlsDisabled(disabled) {
|
|
728
|
+
for (const id of [
|
|
729
|
+
'cursor-allowance-cursor-models-input',
|
|
730
|
+
'cursor-allowance-other-models-input',
|
|
731
|
+
'cursor-allowance-reset-day-input',
|
|
732
|
+
'cursor-allowance-save-btn',
|
|
733
|
+
'cursor-allowance-reset-btn',
|
|
734
|
+
]) {
|
|
735
|
+
const element = document.getElementById(id);
|
|
736
|
+
if (element) element.disabled = disabled;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
export function applyCursorAllowanceState(settings = {}) {
|
|
741
|
+
const cursorModels = document.getElementById('cursor-allowance-cursor-models-input');
|
|
742
|
+
const otherModels = document.getElementById('cursor-allowance-other-models-input');
|
|
743
|
+
const resetDay = document.getElementById('cursor-allowance-reset-day-input');
|
|
744
|
+
const status = document.getElementById('cursor-allowance-status');
|
|
745
|
+
if (cursorModels) {
|
|
746
|
+
cursorModels.value = settings?.cursorModelsUsd === null || settings?.cursorModelsUsd === undefined
|
|
747
|
+
? ''
|
|
748
|
+
: String(settings.cursorModelsUsd);
|
|
749
|
+
}
|
|
750
|
+
if (otherModels) {
|
|
751
|
+
otherModels.value = settings?.otherModelsUsd === null || settings?.otherModelsUsd === undefined
|
|
752
|
+
? ''
|
|
753
|
+
: String(settings.otherModelsUsd);
|
|
754
|
+
}
|
|
755
|
+
if (resetDay) resetDay.value = String(settings?.resetDay ?? 1);
|
|
756
|
+
if (status) {
|
|
757
|
+
const configured = settings?.cursorModelsUsd != null || settings?.otherModelsUsd != null;
|
|
758
|
+
status.textContent = configured
|
|
759
|
+
? `Tracking spend against your allowances; resets on day ${settings?.resetDay ?? 1} of each month.`
|
|
760
|
+
: 'No allowance set — the usage card shows Cursor spend without a remaining balance.';
|
|
761
|
+
status.dataset.state = configured ? 'active' : 'unconfigured';
|
|
762
|
+
}
|
|
763
|
+
setCursorAllowanceControlsDisabled(cursorAllowanceUpdateInFlight);
|
|
764
|
+
return settings;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
export async function refreshCursorAllowanceState() {
|
|
768
|
+
try {
|
|
769
|
+
const settings = await loadCursorAllowanceSettings();
|
|
770
|
+
if (!settings) return null;
|
|
771
|
+
return applyCursorAllowanceState(settings);
|
|
772
|
+
} catch {
|
|
773
|
+
const status = document.getElementById('cursor-allowance-status');
|
|
774
|
+
if (status) {
|
|
775
|
+
status.textContent = 'Unable to load Cursor allowances.';
|
|
776
|
+
status.dataset.state = 'error';
|
|
777
|
+
}
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* Read one allowance field.
|
|
784
|
+
*
|
|
785
|
+
* A `type="number"` input reports an empty `value` for text the browser could
|
|
786
|
+
* not parse, so `validity.badInput` is the only way to tell a deliberately
|
|
787
|
+
* cleared field from "12,50" typed in a comma-decimal locale. Without that
|
|
788
|
+
* check the second case silently posts `null` and wipes the allowance.
|
|
789
|
+
*
|
|
790
|
+
* @returns {number|null|undefined} the amount, null to clear, undefined when
|
|
791
|
+
* the control is absent (leave the stored value alone)
|
|
792
|
+
* @throws {Error} when the field holds something that is not a valid amount
|
|
793
|
+
*/
|
|
794
|
+
function readAllowanceInput(id, label) {
|
|
795
|
+
const input = document.getElementById(id);
|
|
796
|
+
if (!input) return undefined;
|
|
797
|
+
if (input.validity?.badInput) throw new Error(`${label} must be a number, for example 20 or 20.00.`);
|
|
798
|
+
const raw = String(input.value ?? '').trim();
|
|
799
|
+
// An emptied field clears the allowance rather than leaving the old value.
|
|
800
|
+
if (raw === '') return null;
|
|
801
|
+
const parsed = Number(raw);
|
|
802
|
+
if (!Number.isFinite(parsed) || parsed < 0) throw new Error(`${label} must be zero or a positive amount.`);
|
|
803
|
+
return parsed;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function readResetDayInput() {
|
|
807
|
+
const input = document.getElementById('cursor-allowance-reset-day-input');
|
|
808
|
+
if (!input) return undefined;
|
|
809
|
+
if (input.validity?.badInput) throw new Error('Billing reset day must be a whole number between 1 and 31.');
|
|
810
|
+
const raw = String(input.value ?? '').trim();
|
|
811
|
+
if (raw === '') return undefined;
|
|
812
|
+
const parsed = Number(raw);
|
|
813
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 31) {
|
|
814
|
+
throw new Error('Billing reset day must be between 1 and 31.');
|
|
815
|
+
}
|
|
816
|
+
return Math.round(parsed);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
export async function saveCursorAllowanceSettings({ resetAccounting = false } = {}) {
|
|
820
|
+
if (cursorAllowanceUpdateInFlight) return;
|
|
821
|
+
let payload;
|
|
822
|
+
try {
|
|
823
|
+
const resetDay = readResetDayInput();
|
|
824
|
+
payload = {
|
|
825
|
+
cursorModelsUsd: readAllowanceInput('cursor-allowance-cursor-models-input', 'Cursor Models allowance'),
|
|
826
|
+
otherModelsUsd: readAllowanceInput('cursor-allowance-other-models-input', 'Other Models allowance'),
|
|
827
|
+
...(resetDay === undefined ? {} : { resetDay }),
|
|
828
|
+
...(resetAccounting ? { resetAccounting: true } : {}),
|
|
829
|
+
};
|
|
830
|
+
} catch (error) {
|
|
831
|
+
alert(error?.message || 'Please check the Cursor allowance values.');
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
cursorAllowanceUpdateInFlight = true;
|
|
835
|
+
setCursorAllowanceControlsDisabled(true);
|
|
836
|
+
try {
|
|
837
|
+
const result = await updateCursorAllowanceSettings(payload);
|
|
838
|
+
cursorAllowanceUpdateInFlight = false;
|
|
839
|
+
applyCursorAllowanceState(result);
|
|
840
|
+
showTransientRelayNotice(resetAccounting
|
|
841
|
+
? 'Cursor allowances saved and tracked spend reset.'
|
|
842
|
+
: 'Cursor allowances saved.');
|
|
843
|
+
} catch (error) {
|
|
844
|
+
cursorAllowanceUpdateInFlight = false;
|
|
845
|
+
setCursorAllowanceControlsDisabled(false);
|
|
846
|
+
alert(error?.message || 'Failed to update Cursor allowances.');
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
export async function resetCursorAllowanceAccounting() {
|
|
851
|
+
if (!confirm('Reset the tracked Cursor spend for the current billing cycle?')) return;
|
|
852
|
+
await saveCursorAllowanceSettings({ resetAccounting: true });
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// ── Cursor dashboard session token ──
|
|
856
|
+
// Unlocks the live plan-quota bars on the Check Usage card. Only a
|
|
857
|
+
// configured/not-configured flag ever comes back from the server.
|
|
858
|
+
let cursorDashboardTokenUpdateInFlight = false;
|
|
859
|
+
|
|
860
|
+
function setCursorDashboardTokenControlsDisabled(disabled) {
|
|
861
|
+
for (const id of [
|
|
862
|
+
'cursor-dashboard-token-input',
|
|
863
|
+
'cursor-dashboard-token-save-btn',
|
|
864
|
+
'cursor-dashboard-token-remove-btn',
|
|
865
|
+
]) {
|
|
866
|
+
const element = document.getElementById(id);
|
|
867
|
+
if (element) element.disabled = disabled;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
export function applyCursorDashboardTokenState(settings = {}) {
|
|
872
|
+
const status = document.getElementById('cursor-dashboard-token-status');
|
|
873
|
+
const input = document.getElementById('cursor-dashboard-token-input');
|
|
874
|
+
const configured = settings?.configured === true;
|
|
875
|
+
const source = String(settings?.source || '');
|
|
876
|
+
if (input) input.value = '';
|
|
877
|
+
if (input) input.placeholder = source === 'manual' ? 'Token saved — paste a new one to replace it' : 'WorkosCursorSessionToken cookie value';
|
|
878
|
+
if (status) {
|
|
879
|
+
const STATUS_TEXT = {
|
|
880
|
+
ide: "Using this machine's Cursor IDE login automatically — nothing to configure. Paste a token only to override it.",
|
|
881
|
+
env: 'Using the CURSOR_SESSION_TOKEN environment variable on the relay. Paste a token only to override it.',
|
|
882
|
+
};
|
|
883
|
+
status.textContent = STATUS_TEXT[source]
|
|
884
|
+
|| (configured
|
|
885
|
+
? 'Dashboard token saved. The usage card shows live plan bars while it stays valid.'
|
|
886
|
+
: 'No Cursor IDE login found on the relay host and no token pasted — the usage card shows local spend estimates only.');
|
|
887
|
+
status.dataset.state = configured ? 'active' : 'unconfigured';
|
|
888
|
+
}
|
|
889
|
+
setCursorDashboardTokenControlsDisabled(cursorDashboardTokenUpdateInFlight);
|
|
890
|
+
return settings;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
export async function refreshCursorDashboardTokenState() {
|
|
894
|
+
try {
|
|
895
|
+
const settings = await loadCursorDashboardTokenSettings();
|
|
896
|
+
if (!settings) return null;
|
|
897
|
+
return applyCursorDashboardTokenState(settings);
|
|
898
|
+
} catch {
|
|
899
|
+
const status = document.getElementById('cursor-dashboard-token-status');
|
|
900
|
+
if (status) {
|
|
901
|
+
status.textContent = 'Unable to load the Cursor dashboard token state.';
|
|
902
|
+
status.dataset.state = 'error';
|
|
903
|
+
}
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
export async function saveCursorDashboardToken() {
|
|
909
|
+
if (cursorDashboardTokenUpdateInFlight) return;
|
|
910
|
+
const input = document.getElementById('cursor-dashboard-token-input');
|
|
911
|
+
const sessionToken = String(input?.value || '').trim();
|
|
912
|
+
if (!sessionToken) {
|
|
913
|
+
alert('Paste the WorkosCursorSessionToken cookie value first.');
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
cursorDashboardTokenUpdateInFlight = true;
|
|
917
|
+
setCursorDashboardTokenControlsDisabled(true);
|
|
918
|
+
try {
|
|
919
|
+
const result = await updateCursorDashboardTokenSettings({ sessionToken });
|
|
920
|
+
cursorDashboardTokenUpdateInFlight = false;
|
|
921
|
+
applyCursorDashboardTokenState(result);
|
|
922
|
+
} catch (error) {
|
|
923
|
+
cursorDashboardTokenUpdateInFlight = false;
|
|
924
|
+
setCursorDashboardTokenControlsDisabled(false);
|
|
925
|
+
alert(error?.message || 'Failed to save the Cursor dashboard token.');
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
export async function removeCursorDashboardToken() {
|
|
930
|
+
if (cursorDashboardTokenUpdateInFlight) return;
|
|
931
|
+
if (!confirm('Remove the stored Cursor dashboard token?')) return;
|
|
932
|
+
cursorDashboardTokenUpdateInFlight = true;
|
|
933
|
+
setCursorDashboardTokenControlsDisabled(true);
|
|
934
|
+
try {
|
|
935
|
+
const result = await updateCursorDashboardTokenSettings({ remove: true });
|
|
936
|
+
cursorDashboardTokenUpdateInFlight = false;
|
|
937
|
+
applyCursorDashboardTokenState(result);
|
|
938
|
+
} catch (error) {
|
|
939
|
+
cursorDashboardTokenUpdateInFlight = false;
|
|
940
|
+
setCursorDashboardTokenControlsDisabled(false);
|
|
941
|
+
alert(error?.message || 'Failed to remove the Cursor dashboard token.');
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// ── Grok manual plan allowance ──
|
|
946
|
+
// Grok ACP reports per-turn cost/tokens but no remaining plan credits.
|
|
947
|
+
let grokAllowanceUpdateInFlight = false;
|
|
948
|
+
|
|
949
|
+
function setGrokAllowanceControlsDisabled(disabled) {
|
|
950
|
+
for (const id of [
|
|
951
|
+
'grok-allowance-monthly-input',
|
|
952
|
+
'grok-allowance-reset-day-input',
|
|
953
|
+
'grok-allowance-save-btn',
|
|
954
|
+
'grok-allowance-reset-btn',
|
|
955
|
+
]) {
|
|
956
|
+
const element = document.getElementById(id);
|
|
957
|
+
if (element) element.disabled = disabled;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
export function applyGrokAllowanceState(settings = {}) {
|
|
962
|
+
const monthly = document.getElementById('grok-allowance-monthly-input');
|
|
963
|
+
const resetDay = document.getElementById('grok-allowance-reset-day-input');
|
|
964
|
+
const status = document.getElementById('grok-allowance-status');
|
|
965
|
+
if (monthly) {
|
|
966
|
+
monthly.value = settings?.monthlyUsd === null || settings?.monthlyUsd === undefined
|
|
967
|
+
? ''
|
|
968
|
+
: String(settings.monthlyUsd);
|
|
969
|
+
}
|
|
970
|
+
if (resetDay) resetDay.value = String(settings?.resetDay ?? 1);
|
|
971
|
+
if (status) {
|
|
972
|
+
const configured = settings?.monthlyUsd != null;
|
|
973
|
+
status.textContent = configured
|
|
974
|
+
? `Tracking estimated spend against $${settings.monthlyUsd}/mo; resets on day ${settings?.resetDay ?? 1}.`
|
|
975
|
+
: 'No allowance set — Check Usage shows last-turn Grok cost/tokens without a remaining-budget meter.';
|
|
976
|
+
status.dataset.state = configured ? 'active' : 'unconfigured';
|
|
977
|
+
}
|
|
978
|
+
setGrokAllowanceControlsDisabled(grokAllowanceUpdateInFlight);
|
|
979
|
+
return settings;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
export async function refreshGrokAllowanceState() {
|
|
983
|
+
try {
|
|
984
|
+
const settings = await loadGrokAllowanceSettings();
|
|
985
|
+
if (!settings) return null;
|
|
986
|
+
return applyGrokAllowanceState(settings);
|
|
987
|
+
} catch {
|
|
988
|
+
const status = document.getElementById('grok-allowance-status');
|
|
989
|
+
if (status) {
|
|
990
|
+
status.textContent = 'Unable to load Grok allowances.';
|
|
991
|
+
status.dataset.state = 'error';
|
|
992
|
+
}
|
|
993
|
+
return null;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
function readGrokResetDayInput() {
|
|
998
|
+
const input = document.getElementById('grok-allowance-reset-day-input');
|
|
999
|
+
if (!input) return undefined;
|
|
1000
|
+
if (input.validity?.badInput) throw new Error('Billing reset day must be a whole number between 1 and 31.');
|
|
1001
|
+
const raw = String(input.value ?? '').trim();
|
|
1002
|
+
if (raw === '') return undefined;
|
|
1003
|
+
const parsed = Number(raw);
|
|
1004
|
+
if (!Number.isFinite(parsed) || parsed < 1 || parsed > 31) {
|
|
1005
|
+
throw new Error('Billing reset day must be between 1 and 31.');
|
|
1006
|
+
}
|
|
1007
|
+
return Math.round(parsed);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
export async function saveGrokAllowanceSettings({ resetAccounting = false } = {}) {
|
|
1011
|
+
if (grokAllowanceUpdateInFlight) return;
|
|
1012
|
+
let payload;
|
|
1013
|
+
try {
|
|
1014
|
+
const resetDay = readGrokResetDayInput();
|
|
1015
|
+
payload = {
|
|
1016
|
+
monthlyUsd: readAllowanceInput('grok-allowance-monthly-input', 'Grok monthly allowance'),
|
|
1017
|
+
...(resetDay === undefined ? {} : { resetDay }),
|
|
1018
|
+
...(resetAccounting ? { resetAccounting: true } : {}),
|
|
1019
|
+
};
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
alert(error?.message || 'Please check the Grok allowance values.');
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
grokAllowanceUpdateInFlight = true;
|
|
1025
|
+
setGrokAllowanceControlsDisabled(true);
|
|
1026
|
+
try {
|
|
1027
|
+
const result = await updateGrokAllowanceSettings(payload);
|
|
1028
|
+
grokAllowanceUpdateInFlight = false;
|
|
1029
|
+
applyGrokAllowanceState(result);
|
|
1030
|
+
showTransientRelayNotice(resetAccounting
|
|
1031
|
+
? 'Grok allowances saved and tracked spend reset.'
|
|
1032
|
+
: 'Grok allowances saved.');
|
|
1033
|
+
} catch (error) {
|
|
1034
|
+
grokAllowanceUpdateInFlight = false;
|
|
1035
|
+
setGrokAllowanceControlsDisabled(false);
|
|
1036
|
+
alert(error?.message || 'Failed to update Grok allowances.');
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
export async function resetGrokAllowanceAccounting() {
|
|
1041
|
+
if (!confirm('Reset the tracked Grok spend for the current billing cycle?')) return;
|
|
1042
|
+
await saveGrokAllowanceSettings({ resetAccounting: true });
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
async function syncCursorSettingsInputs() {
|
|
1046
|
+
const keyInput = document.getElementById('cursor-api-key-input');
|
|
1047
|
+
const modelInput = document.getElementById('cursor-model-input');
|
|
1048
|
+
const status = document.getElementById('cursor-settings-status');
|
|
1049
|
+
if (!keyInput || !modelInput || !status) return;
|
|
1050
|
+
const settings = await refreshCursorSettingsState();
|
|
1051
|
+
if (!settings) {
|
|
1052
|
+
status.textContent = 'Unable to load Cursor settings.';
|
|
1053
|
+
status.dataset.state = 'error';
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
export async function saveCursorSettings() {
|
|
1059
|
+
if (cursorSettingsUpdateInFlight) return;
|
|
1060
|
+
const keyInput = document.getElementById('cursor-api-key-input');
|
|
1061
|
+
const modelInput = document.getElementById('cursor-model-input');
|
|
1062
|
+
const apiKey = String(keyInput?.value || '').trim();
|
|
1063
|
+
const model = String(modelInput?.value || '').trim() || 'composer-2.5';
|
|
1064
|
+
if (!apiKey && !cursorSettingsState.configured) {
|
|
1065
|
+
alert('Enter a Cursor API key.');
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
cursorSettingsUpdateInFlight = true;
|
|
1069
|
+
setCursorSettingsControlsDisabled(true);
|
|
1070
|
+
try {
|
|
1071
|
+
const result = await updateCursorSettings({
|
|
1072
|
+
apiKey,
|
|
1073
|
+
model,
|
|
1074
|
+
enabled: cursorSettingsState.configured ? cursorSettingsState.enabled : true,
|
|
1075
|
+
});
|
|
1076
|
+
if (!result) throw new Error('Failed to save Cursor settings.');
|
|
1077
|
+
applyCursorSettingsState(result, { resetInputs: true });
|
|
1078
|
+
showTransientRelayNotice(
|
|
1079
|
+
result.warning
|
|
1080
|
+
? `Cursor settings saved. ${result.warning}`
|
|
1081
|
+
: `Cursor settings saved for ${result.model}.`,
|
|
1082
|
+
result.warning ? 8000 : 4000,
|
|
1083
|
+
);
|
|
1084
|
+
} catch (error) {
|
|
1085
|
+
alert(error?.message || 'Failed to save Cursor settings.');
|
|
1086
|
+
} finally {
|
|
1087
|
+
cursorSettingsUpdateInFlight = false;
|
|
1088
|
+
setCursorSettingsControlsDisabled(false);
|
|
1089
|
+
applyCursorSettingsState(cursorSettingsState);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
export async function toggleCursorProvider(enabled) {
|
|
1094
|
+
if (cursorSettingsUpdateInFlight) return;
|
|
1095
|
+
if (enabled && !cursorSettingsState.configured) {
|
|
1096
|
+
applyCursorSettingsState(cursorSettingsState);
|
|
1097
|
+
alert('Save a Cursor API key before enabling Cursor.');
|
|
1098
|
+
return;
|
|
1099
|
+
}
|
|
1100
|
+
cursorSettingsUpdateInFlight = true;
|
|
1101
|
+
setCursorSettingsControlsDisabled(true);
|
|
1102
|
+
try {
|
|
1103
|
+
const result = await updateCursorSettings({
|
|
1104
|
+
model: cursorSettingsState.model,
|
|
1105
|
+
enabled: enabled === true,
|
|
1106
|
+
});
|
|
1107
|
+
if (!result) throw new Error('Failed to update the Cursor provider.');
|
|
1108
|
+
applyCursorSettingsState(result);
|
|
1109
|
+
const providerLabel = result.enabled ? 'Cursor API key enabled' : 'Cursor API key disabled';
|
|
1110
|
+
showTransientRelayNotice(
|
|
1111
|
+
`${providerLabel}.${result.warning ? ` ${result.warning}` : ''}`,
|
|
1112
|
+
result.warning ? 8000 : 4500,
|
|
1113
|
+
);
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
applyCursorSettingsState(cursorSettingsState);
|
|
1116
|
+
alert(error?.message || 'Failed to update the Cursor provider.');
|
|
1117
|
+
} finally {
|
|
1118
|
+
cursorSettingsUpdateInFlight = false;
|
|
1119
|
+
setCursorSettingsControlsDisabled(false);
|
|
1120
|
+
applyCursorSettingsState(cursorSettingsState);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
export async function removeCursorSettings() {
|
|
1125
|
+
if (cursorSettingsUpdateInFlight) return;
|
|
1126
|
+
if (!cursorSettingsState.configured) return;
|
|
1127
|
+
if (!confirm('Remove the saved Cursor API key?')) return;
|
|
1128
|
+
const modelInput = document.getElementById('cursor-model-input');
|
|
1129
|
+
const model = String(modelInput?.value || '').trim() || 'composer-2.5';
|
|
1130
|
+
cursorSettingsUpdateInFlight = true;
|
|
1131
|
+
setCursorSettingsControlsDisabled(true);
|
|
1132
|
+
try {
|
|
1133
|
+
const result = await updateCursorSettings({ model, remove: true });
|
|
1134
|
+
if (!result) throw new Error('Failed to remove Cursor settings.');
|
|
1135
|
+
applyCursorSettingsState(result, { resetInputs: true });
|
|
1136
|
+
showTransientRelayNotice('Cursor API key removed. New conversations use GitHub Copilot.');
|
|
1137
|
+
} catch (error) {
|
|
1138
|
+
// A 409 'cursor-key-removal-blocked' rejection carries the active
|
|
1139
|
+
// conversation counts inside the server's error message.
|
|
1140
|
+
alert(error?.message || 'Failed to remove Cursor settings.');
|
|
1141
|
+
} finally {
|
|
1142
|
+
cursorSettingsUpdateInFlight = false;
|
|
1143
|
+
setCursorSettingsControlsDisabled(false);
|
|
1144
|
+
applyCursorSettingsState(cursorSettingsState);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
async function syncOpenAISettingsInputs() {
|
|
1149
|
+
const keyInput = document.getElementById('openai-api-key-input');
|
|
1150
|
+
const modelInput = document.getElementById('openai-model-input');
|
|
1151
|
+
const status = document.getElementById('openai-settings-status');
|
|
1152
|
+
if (!keyInput || !modelInput || !status) return;
|
|
1153
|
+
const settings = await refreshOpenAISettingsState();
|
|
1154
|
+
if (!settings) {
|
|
1155
|
+
status.textContent = 'Unable to load OpenAI settings.';
|
|
1156
|
+
status.dataset.state = 'error';
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
export async function saveOpenAISettings() {
|
|
1162
|
+
if (openAISettingsUpdateInFlight) return;
|
|
1163
|
+
const keyInput = document.getElementById('openai-api-key-input');
|
|
1164
|
+
const modelInput = document.getElementById('openai-model-input');
|
|
1165
|
+
const baseUrlInput = document.getElementById('openai-base-url-input');
|
|
1166
|
+
const apiKey = String(keyInput?.value || '').trim();
|
|
1167
|
+
const model = String(modelInput?.value || '').trim() || 'gpt-4o';
|
|
1168
|
+
const baseUrl = String(baseUrlInput?.value || '').trim() || 'https://api.openai.com/v1';
|
|
1169
|
+
if (!apiKey && !openAISettingsState.configured) {
|
|
1170
|
+
alert('Enter an OpenAI API key.');
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
openAISettingsUpdateInFlight = true;
|
|
1174
|
+
setOpenAISettingsControlsDisabled(true);
|
|
1175
|
+
try {
|
|
1176
|
+
const result = await updateOpenAISettings({
|
|
1177
|
+
apiKey,
|
|
1178
|
+
model,
|
|
1179
|
+
baseUrl,
|
|
1180
|
+
enabled: openAISettingsState.configured ? openAISettingsState.enabled : true,
|
|
1181
|
+
});
|
|
1182
|
+
if (!result) throw new Error('Failed to save OpenAI settings.');
|
|
1183
|
+
applyOpenAISettingsState(result, { resetInputs: true });
|
|
1184
|
+
showTransientRelayNotice(
|
|
1185
|
+
result.warning
|
|
1186
|
+
? `OpenAI settings saved. ${result.warning}`
|
|
1187
|
+
: `OpenAI settings saved for ${result.model}.`,
|
|
1188
|
+
result.warning ? 8000 : 4000,
|
|
1189
|
+
);
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
alert(error?.message || 'Failed to save OpenAI settings.');
|
|
1192
|
+
} finally {
|
|
1193
|
+
openAISettingsUpdateInFlight = false;
|
|
1194
|
+
setOpenAISettingsControlsDisabled(false);
|
|
1195
|
+
applyOpenAISettingsState(openAISettingsState);
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
export async function toggleOpenAIProvider(enabled) {
|
|
1200
|
+
if (openAISettingsUpdateInFlight) return;
|
|
1201
|
+
if (enabled && !openAISettingsState.configured) {
|
|
1202
|
+
applyOpenAISettingsState(openAISettingsState);
|
|
1203
|
+
alert('Save an OpenAI API key before enabling OpenAI.');
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
openAISettingsUpdateInFlight = true;
|
|
1207
|
+
setOpenAISettingsControlsDisabled(true);
|
|
1208
|
+
try {
|
|
1209
|
+
const result = await updateOpenAISettings({
|
|
1210
|
+
model: openAISettingsState.model,
|
|
1211
|
+
baseUrl: openAISettingsState.baseUrl,
|
|
1212
|
+
enabled: enabled === true,
|
|
1213
|
+
});
|
|
1214
|
+
if (!result) throw new Error('Failed to update OpenAI provider.');
|
|
1215
|
+
applyOpenAISettingsState(result);
|
|
1216
|
+
const providerLabel = result.enabled ? 'OpenAI API key enabled' : 'OpenAI API key disabled';
|
|
1217
|
+
showTransientRelayNotice(
|
|
1218
|
+
`${providerLabel}.${result.warning ? ` ${result.warning}` : ''}`,
|
|
1219
|
+
result.warning ? 8000 : 4500,
|
|
1220
|
+
);
|
|
1221
|
+
} catch (error) {
|
|
1222
|
+
applyOpenAISettingsState(openAISettingsState);
|
|
1223
|
+
alert(error?.message || 'Failed to update OpenAI provider.');
|
|
1224
|
+
} finally {
|
|
1225
|
+
openAISettingsUpdateInFlight = false;
|
|
1226
|
+
setOpenAISettingsControlsDisabled(false);
|
|
1227
|
+
applyOpenAISettingsState(openAISettingsState);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
export async function removeOpenAISettings() {
|
|
1232
|
+
if (openAISettingsUpdateInFlight) return;
|
|
1233
|
+
if (!openAISettingsState.configured) return;
|
|
1234
|
+
if (!confirm('Remove the saved OpenAI API key?')) return;
|
|
1235
|
+
const modelInput = document.getElementById('openai-model-input');
|
|
1236
|
+
const model = String(modelInput?.value || '').trim() || 'gpt-4o';
|
|
1237
|
+
openAISettingsUpdateInFlight = true;
|
|
1238
|
+
setOpenAISettingsControlsDisabled(true);
|
|
1239
|
+
try {
|
|
1240
|
+
const result = await updateOpenAISettings({ model, remove: true });
|
|
1241
|
+
if (!result) throw new Error('Failed to remove OpenAI settings.');
|
|
1242
|
+
applyOpenAISettingsState(result, { resetInputs: true });
|
|
1243
|
+
showTransientRelayNotice('OpenAI API key removed. New conversations use GitHub Copilot.');
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
alert(error?.message || 'Failed to remove OpenAI settings.');
|
|
1246
|
+
} finally {
|
|
1247
|
+
openAISettingsUpdateInFlight = false;
|
|
1248
|
+
setOpenAISettingsControlsDisabled(false);
|
|
1249
|
+
applyOpenAISettingsState(openAISettingsState);
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function syncWindowsAutostartSetting() {
|
|
1254
|
+
const container = document.getElementById('windows-autostart-setting');
|
|
1255
|
+
const checkbox = document.getElementById('windows-autostart-toggle');
|
|
1256
|
+
const supported = serverPlatform === 'win32';
|
|
1257
|
+
if (container) container.hidden = !supported;
|
|
1258
|
+
if (checkbox instanceof HTMLInputElement) {
|
|
1259
|
+
checkbox.checked = windowsAutostartEnabled;
|
|
1260
|
+
checkbox.disabled = !supported || windowsAutostartUpdateInFlight;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
export async function refreshWindowsAutostartSetting() {
|
|
1265
|
+
syncWindowsAutostartSetting();
|
|
1266
|
+
if (serverPlatform !== 'win32' || windowsAutostartUpdateInFlight) return;
|
|
1267
|
+
windowsAutostartUpdateInFlight = true;
|
|
1268
|
+
syncWindowsAutostartSetting();
|
|
1269
|
+
try {
|
|
1270
|
+
const result = await loadWindowsAutostartSetting();
|
|
1271
|
+
if (!result?.supported) {
|
|
1272
|
+
alert('Failed to read the Windows autostart setting.');
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
windowsAutostartEnabled = !!result.enabled;
|
|
1276
|
+
} catch (error) {
|
|
1277
|
+
alert(error?.message || 'Failed to read the Windows autostart setting.');
|
|
1278
|
+
} finally {
|
|
1279
|
+
windowsAutostartUpdateInFlight = false;
|
|
1280
|
+
syncWindowsAutostartSetting();
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
export async function updateWindowsAutostartSettingFromToggle(enabled) {
|
|
1285
|
+
if (serverPlatform !== 'win32' || windowsAutostartUpdateInFlight) {
|
|
1286
|
+
syncWindowsAutostartSetting();
|
|
1287
|
+
return;
|
|
1288
|
+
}
|
|
1289
|
+
windowsAutostartUpdateInFlight = true;
|
|
1290
|
+
syncWindowsAutostartSetting();
|
|
1291
|
+
try {
|
|
1292
|
+
const result = await updateWindowsAutostartSetting(!!enabled);
|
|
1293
|
+
if (!result?.supported) {
|
|
1294
|
+
alert('Failed to update the Windows autostart setting.');
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
windowsAutostartEnabled = !!result.enabled;
|
|
1298
|
+
showTransientRelayNotice(
|
|
1299
|
+
windowsAutostartEnabled
|
|
1300
|
+
? 'Windows autostart enabled.'
|
|
1301
|
+
: 'Windows autostart disabled.',
|
|
1302
|
+
);
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
alert(error?.message || 'Failed to update the Windows autostart setting.');
|
|
1305
|
+
} finally {
|
|
1306
|
+
windowsAutostartUpdateInFlight = false;
|
|
1307
|
+
syncWindowsAutostartSetting();
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function readShowSuspendHostSetting() {
|
|
1312
|
+
const stored = String(readLocalStorage(SHOW_SUSPEND_HOST_STORAGE_KEY) || '').trim().toLowerCase();
|
|
1313
|
+
if (!stored) return true;
|
|
1314
|
+
return stored !== '0' && stored !== 'false';
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
function setShowSuspendHostSetting(show, { persist = true } = {}) {
|
|
1318
|
+
const next = !!show;
|
|
1319
|
+
if (persist) writeLocalStorage(SHOW_SUSPEND_HOST_STORAGE_KEY, next ? '1' : '0');
|
|
1320
|
+
return next;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
export function isSuspendHostActionVisible() {
|
|
1324
|
+
return readShowSuspendHostSetting();
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
export function syncSuspendHostVisibility() {
|
|
1328
|
+
const show = isSuspendHostActionVisible();
|
|
1329
|
+
const menuBtn = document.getElementById('chat-menu-suspend-host');
|
|
1330
|
+
const checkbox = document.getElementById('show-suspend-host-toggle');
|
|
1331
|
+
if (menuBtn) {
|
|
1332
|
+
menuBtn.hidden = !show;
|
|
1333
|
+
menuBtn.disabled = !show;
|
|
1334
|
+
menuBtn.tabIndex = show ? 0 : -1;
|
|
1335
|
+
menuBtn.setAttribute('aria-hidden', show ? 'false' : 'true');
|
|
1336
|
+
}
|
|
1337
|
+
if (checkbox instanceof HTMLInputElement) {
|
|
1338
|
+
checkbox.checked = show;
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
export function initTheme() {
|
|
1343
|
+
const saved = readLocalStorage(THEME_STORAGE_KEY);
|
|
1344
|
+
if (saved === 'light') {
|
|
1345
|
+
document.documentElement.setAttribute('data-theme', 'light');
|
|
1346
|
+
} else {
|
|
1347
|
+
document.documentElement.removeAttribute('data-theme');
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
export function updateTheme(theme) {
|
|
1352
|
+
if (theme === 'light') {
|
|
1353
|
+
document.documentElement.setAttribute('data-theme', 'light');
|
|
1354
|
+
writeLocalStorage(THEME_STORAGE_KEY, 'light');
|
|
1355
|
+
} else {
|
|
1356
|
+
document.documentElement.removeAttribute('data-theme');
|
|
1357
|
+
writeLocalStorage(THEME_STORAGE_KEY, 'dark');
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
export function updateShowSuspendHostSetting(next) {
|
|
1362
|
+
setShowSuspendHostSetting(next, { persist: true });
|
|
1363
|
+
syncSuspendHostVisibility();
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
export async function updateDefaultSessionWorkspaceRootSetting(rawValue) {
|
|
1367
|
+
if (defaultSessionWorkspaceRootUpdateInFlight) {
|
|
1368
|
+
syncDefaultSessionWorkspaceRootInput();
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
const normalizedPath = normalizeKnownCwdPath(rawValue);
|
|
1372
|
+
defaultSessionWorkspaceRootUpdateInFlight = true;
|
|
1373
|
+
try {
|
|
1374
|
+
const result = await updateDefaultSessionWorkspaceRoot(normalizedPath, {
|
|
1375
|
+
clear: !normalizedPath,
|
|
1376
|
+
});
|
|
1377
|
+
if (!result) {
|
|
1378
|
+
alert('Failed to update the default CWD for new sessions.');
|
|
1379
|
+
syncDefaultSessionWorkspaceRootInput();
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
syncDefaultSessionWorkspaceRootInput();
|
|
1383
|
+
if (result.defaultSessionWorkspaceRootWarning) {
|
|
1384
|
+
showTransientRelayNotice(String(result.defaultSessionWorkspaceRootWarning), 7000);
|
|
1385
|
+
}
|
|
1386
|
+
if (normalizedPath) {
|
|
1387
|
+
const savedPath = String(result.defaultSessionWorkspaceRootPath || normalizedPath).trim();
|
|
1388
|
+
showTransientRelayNotice(`Default CWD for new sessions saved as ${savedPath}.`);
|
|
1389
|
+
} else {
|
|
1390
|
+
showTransientRelayNotice('Default CWD reset. New sessions will use relay workspace root.');
|
|
1391
|
+
}
|
|
1392
|
+
} catch (error) {
|
|
1393
|
+
alert(error?.message || 'Failed to update the default CWD for new sessions.');
|
|
1394
|
+
syncDefaultSessionWorkspaceRootInput();
|
|
1395
|
+
} finally {
|
|
1396
|
+
defaultSessionWorkspaceRootUpdateInFlight = false;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// "Live previews": every preview across every session, not just this
|
|
1401
|
+
// conversation's. Previews never expire, so this list is the one place that can
|
|
1402
|
+
// close a link left behind in a conversation you have moved on from.
|
|
1403
|
+
export function renderPreviewsSection() {
|
|
1404
|
+
const list = document.getElementById('settings-previews-list');
|
|
1405
|
+
const empty = document.getElementById('settings-previews-empty');
|
|
1406
|
+
if (!list) return;
|
|
1407
|
+
const previews = getPreviews();
|
|
1408
|
+
renderPreviewRowsInto(list, previews, { withConversation: true });
|
|
1409
|
+
if (empty) {
|
|
1410
|
+
empty.hidden = previews.length > 0;
|
|
1411
|
+
empty.textContent = 'No previews are published right now.';
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
export async function refreshPreviewsSection() {
|
|
1416
|
+
try {
|
|
1417
|
+
// The socket keeps the store live, but a modal opened right after a reload
|
|
1418
|
+
// may not have seen an event yet.
|
|
1419
|
+
const response = await apiFetch('/api/previews');
|
|
1420
|
+
setPreviews(response?.previews || []);
|
|
1421
|
+
} catch {
|
|
1422
|
+
renderPreviewsSection();
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
subscribePreviews(() => {
|
|
1427
|
+
const modal = document.getElementById('settings-modal');
|
|
1428
|
+
if (!modal?.classList.contains('visible')) return;
|
|
1429
|
+
renderPreviewsSection();
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
// `tab` / `providerTab` are optional deep links (e.g. openSettingsModal('providers', 'claude'));
|
|
1433
|
+
// omitting them restores the last tab the user was on.
|
|
1434
|
+
export function openSettingsModal(tab, providerTab) {
|
|
1435
|
+
closeChatActionsMenu();
|
|
1436
|
+
const modal = document.getElementById('settings-modal');
|
|
1437
|
+
selectSettingsTab(tab, providerTab);
|
|
1438
|
+
const themeSelect = document.getElementById('theme-select');
|
|
1439
|
+
if (themeSelect) {
|
|
1440
|
+
themeSelect.value = readLocalStorage(THEME_STORAGE_KEY) === 'light' ? 'light' : 'dark';
|
|
1441
|
+
}
|
|
1442
|
+
syncSuspendHostVisibility();
|
|
1443
|
+
syncFontScaleSelect();
|
|
1444
|
+
syncPwaAppNameInput();
|
|
1445
|
+
syncDefaultSessionWorkspaceRootInput();
|
|
1446
|
+
void syncCopilotSettingsInputs();
|
|
1447
|
+
openAISettingsInputsDirty = false;
|
|
1448
|
+
ensureOpenAISettingsInputTracking();
|
|
1449
|
+
void syncOpenAISettingsInputs();
|
|
1450
|
+
claudeSettingsInputsDirty = false;
|
|
1451
|
+
ensureClaudeSettingsInputTracking();
|
|
1452
|
+
void syncClaudeSettingsInputs();
|
|
1453
|
+
void refreshClaudeAuthSection();
|
|
1454
|
+
void refreshGrokAuthSection();
|
|
1455
|
+
// Forced: the cached rows can be 30s stale, and a CLI installed from a shell
|
|
1456
|
+
// in the meantime should show up the moment the panel opens. Sockets keep it
|
|
1457
|
+
// current from here on.
|
|
1458
|
+
void refreshCliInstallSections({ force: true });
|
|
1459
|
+
grokSettingsInputsDirty = false;
|
|
1460
|
+
ensureGrokSettingsInputTracking();
|
|
1461
|
+
void syncGrokSettingsInputs();
|
|
1462
|
+
cursorSettingsInputsDirty = false;
|
|
1463
|
+
ensureCursorSettingsInputTracking();
|
|
1464
|
+
void syncCursorSettingsInputs();
|
|
1465
|
+
void refreshCursorAllowanceState();
|
|
1466
|
+
void refreshCursorDashboardTokenState();
|
|
1467
|
+
void refreshGrokAllowanceState();
|
|
1468
|
+
syncWindowsAutostartSetting();
|
|
1469
|
+
void refreshWindowsAutostartSetting();
|
|
1470
|
+
syncTurnCeilingSlider();
|
|
1471
|
+
void refreshTurnCeilingSetting();
|
|
1472
|
+
syncBackgroundTaskTimeoutSlider();
|
|
1473
|
+
void refreshBackgroundTaskTimeoutSetting();
|
|
1474
|
+
void refreshPushSettingsSection();
|
|
1475
|
+
void refreshPreviewsSection();
|
|
1476
|
+
modal?.classList.add('visible');
|
|
1477
|
+
modal?.setAttribute('aria-hidden', 'false');
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
export function closeSettingsModal() {
|
|
1481
|
+
const modal = document.getElementById('settings-modal');
|
|
1482
|
+
modal?.classList.remove('visible');
|
|
1483
|
+
modal?.setAttribute('aria-hidden', 'true');
|
|
1484
|
+
}
|