@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,129 @@
|
|
|
1
|
+
const SETTINGS_TAB_STORAGE_KEY = 'copilot_settings_tab';
|
|
2
|
+
const SETTINGS_PROVIDER_TAB_STORAGE_KEY = 'copilot_settings_provider_tab';
|
|
3
|
+
|
|
4
|
+
const SETTINGS_TABS = ['general', 'providers', 'previews', 'notifications'];
|
|
5
|
+
// Copilot leads: it is the default provider, and `SETTINGS_PROVIDER_TABS[0]`
|
|
6
|
+
// is also the sub-tab a first-time visitor lands on.
|
|
7
|
+
const SETTINGS_PROVIDER_TABS = ['copilot', 'openai', 'claude', 'grok', 'cursor'];
|
|
8
|
+
|
|
9
|
+
let settingsTabsBound = false;
|
|
10
|
+
|
|
11
|
+
function readLocalStorage(key) {
|
|
12
|
+
try {
|
|
13
|
+
return localStorage.getItem(key);
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function writeLocalStorage(key, value) {
|
|
20
|
+
try {
|
|
21
|
+
localStorage.setItem(key, value);
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeTab(raw, allowed) {
|
|
29
|
+
const value = String(raw || '').trim().toLowerCase();
|
|
30
|
+
return allowed.includes(value) ? value : '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function storedSettingsTab() {
|
|
34
|
+
return normalizeTab(readLocalStorage(SETTINGS_TAB_STORAGE_KEY), SETTINGS_TABS) || SETTINGS_TABS[0];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function storedProviderTab() {
|
|
38
|
+
return normalizeTab(readLocalStorage(SETTINGS_PROVIDER_TAB_STORAGE_KEY), SETTINGS_PROVIDER_TABS)
|
|
39
|
+
|| SETTINGS_PROVIDER_TABS[0];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function settingsElements(attribute) {
|
|
43
|
+
return Array.from(document.querySelectorAll(`#settings-modal [${attribute}]`));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// One strip + its panels: mark the selected button/panel and hide the rest.
|
|
47
|
+
function applyTabState(buttonAttribute, panelAttribute, active) {
|
|
48
|
+
const buttons = settingsElements(buttonAttribute);
|
|
49
|
+
if (!buttons.length) return false;
|
|
50
|
+
for (const button of buttons) {
|
|
51
|
+
const selected = button.getAttribute(buttonAttribute) === active;
|
|
52
|
+
button.classList.toggle('active', selected);
|
|
53
|
+
button.setAttribute('aria-selected', selected ? 'true' : 'false');
|
|
54
|
+
button.tabIndex = selected ? 0 : -1;
|
|
55
|
+
}
|
|
56
|
+
for (const panel of settingsElements(panelAttribute)) {
|
|
57
|
+
const selected = panel.getAttribute(panelAttribute) === active;
|
|
58
|
+
panel.classList.toggle('active', selected);
|
|
59
|
+
panel.hidden = !selected;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function focusTabByOffset(strip, current, offset) {
|
|
65
|
+
const buttons = Array.from(strip.querySelectorAll('[data-settings-tab], [data-settings-provider-tab]'))
|
|
66
|
+
.filter((button) => !button.disabled);
|
|
67
|
+
if (!buttons.length) return;
|
|
68
|
+
const index = buttons.indexOf(current);
|
|
69
|
+
const next = offset === 'home'
|
|
70
|
+
? buttons[0]
|
|
71
|
+
: offset === 'end'
|
|
72
|
+
? buttons[buttons.length - 1]
|
|
73
|
+
: buttons[(index + offset + buttons.length) % buttons.length];
|
|
74
|
+
if (!next) return;
|
|
75
|
+
next.focus();
|
|
76
|
+
next.click();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function handleTabKeydown(event) {
|
|
80
|
+
const button = event.target?.closest?.('[data-settings-tab], [data-settings-provider-tab]');
|
|
81
|
+
if (!button) return;
|
|
82
|
+
const strip = button.closest('.settings-tab-strip');
|
|
83
|
+
if (!strip) return;
|
|
84
|
+
const offset = {
|
|
85
|
+
ArrowRight: 1,
|
|
86
|
+
ArrowDown: 1,
|
|
87
|
+
ArrowLeft: -1,
|
|
88
|
+
ArrowUp: -1,
|
|
89
|
+
Home: 'home',
|
|
90
|
+
End: 'end',
|
|
91
|
+
}[event.key];
|
|
92
|
+
if (offset === undefined) return;
|
|
93
|
+
event.preventDefault();
|
|
94
|
+
focusTabByOffset(strip, button, offset);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function handleTabClick(event) {
|
|
98
|
+
const topTab = event.target?.closest?.('[data-settings-tab]');
|
|
99
|
+
if (topTab) {
|
|
100
|
+
selectSettingsTab(topTab.dataset.settingsTab);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const providerTab = event.target?.closest?.('[data-settings-provider-tab]');
|
|
104
|
+
if (providerTab) selectSettingsTab(null, providerTab.dataset.settingsProviderTab);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function ensureSettingsTabsBound() {
|
|
108
|
+
if (settingsTabsBound) return;
|
|
109
|
+
const modal = document.getElementById('settings-modal');
|
|
110
|
+
if (!modal) return;
|
|
111
|
+
settingsTabsBound = true;
|
|
112
|
+
modal.addEventListener('click', handleTabClick);
|
|
113
|
+
modal.addEventListener('keydown', handleTabKeydown);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// `tab` / `providerTab` are optional: anything omitted (or unknown) falls back
|
|
117
|
+
// to the last selection persisted in localStorage.
|
|
118
|
+
export function selectSettingsTab(tab, providerTab) {
|
|
119
|
+
ensureSettingsTabsBound();
|
|
120
|
+
const nextTab = normalizeTab(tab, SETTINGS_TABS) || storedSettingsTab();
|
|
121
|
+
const nextProviderTab = normalizeTab(providerTab, SETTINGS_PROVIDER_TABS) || storedProviderTab();
|
|
122
|
+
if (applyTabState('data-settings-tab', 'data-settings-panel', nextTab)) {
|
|
123
|
+
writeLocalStorage(SETTINGS_TAB_STORAGE_KEY, nextTab);
|
|
124
|
+
}
|
|
125
|
+
if (applyTabState('data-settings-provider-tab', 'data-settings-provider-panel', nextProviderTab)) {
|
|
126
|
+
writeLocalStorage(SETTINGS_PROVIDER_TAB_STORAGE_KEY, nextProviderTab);
|
|
127
|
+
}
|
|
128
|
+
return { tab: nextTab, providerTab: nextProviderTab };
|
|
129
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// The composer's slash-command autocompletion menu. Rendering and keyboard
|
|
2
|
+
// only — what to offer comes from slash-commands.mjs, live preview tokens from
|
|
3
|
+
// preview-cards.mjs, and the composer wires both in (conversation-view calls
|
|
4
|
+
// update on input and offers every keydown here first).
|
|
5
|
+
//
|
|
6
|
+
// Enter contract: nothing is highlighted by default, so plain Enter keeps its
|
|
7
|
+
// newline and Ctrl/Cmd+Enter keeps sending; only an arrow-selected row makes
|
|
8
|
+
// Enter accept. Tab always accepts the top insertable match.
|
|
9
|
+
|
|
10
|
+
import { completionsFor } from './slash-commands.mjs';
|
|
11
|
+
import { getPreviews } from './preview-cards.mjs';
|
|
12
|
+
|
|
13
|
+
let openState = null; // { items, replaceRange } while the menu is visible
|
|
14
|
+
let selectedIndex = -1;
|
|
15
|
+
|
|
16
|
+
function popupEl() {
|
|
17
|
+
return document.getElementById('slash-autocomplete-popup');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isSlashAutocompleteOpen() {
|
|
21
|
+
return openState !== null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function closeSlashAutocomplete() {
|
|
25
|
+
openState = null;
|
|
26
|
+
selectedIndex = -1;
|
|
27
|
+
const popup = popupEl();
|
|
28
|
+
if (!popup) return;
|
|
29
|
+
popup.classList.remove('visible');
|
|
30
|
+
popup.setAttribute('aria-hidden', 'true');
|
|
31
|
+
popup.textContent = '';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function insertableIndexes() {
|
|
35
|
+
return (openState?.items || [])
|
|
36
|
+
.map((item, index) => (item.insert !== null ? index : -1))
|
|
37
|
+
.filter((index) => index !== -1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function renderMenu() {
|
|
41
|
+
const popup = popupEl();
|
|
42
|
+
if (!popup || !openState) return;
|
|
43
|
+
popup.textContent = '';
|
|
44
|
+
openState.items.forEach((item, index) => {
|
|
45
|
+
const row = document.createElement('div');
|
|
46
|
+
row.className = item.kind === 'hint' ? 'slash-item slash-item-hint' : 'slash-item';
|
|
47
|
+
if (index === selectedIndex) row.classList.add('slash-item-selected');
|
|
48
|
+
row.setAttribute('role', 'option');
|
|
49
|
+
row.setAttribute('aria-selected', index === selectedIndex ? 'true' : 'false');
|
|
50
|
+
row.dataset.index = String(index);
|
|
51
|
+
|
|
52
|
+
const nameEl = document.createElement('span');
|
|
53
|
+
nameEl.className = 'slash-item-name';
|
|
54
|
+
nameEl.textContent = item.display;
|
|
55
|
+
row.appendChild(nameEl);
|
|
56
|
+
|
|
57
|
+
if (item.description) {
|
|
58
|
+
// textContent throughout: preview labels are agent-supplied strings.
|
|
59
|
+
const descEl = document.createElement('span');
|
|
60
|
+
descEl.className = 'slash-item-desc';
|
|
61
|
+
descEl.textContent = item.description;
|
|
62
|
+
row.appendChild(descEl);
|
|
63
|
+
}
|
|
64
|
+
popup.appendChild(row);
|
|
65
|
+
});
|
|
66
|
+
popup.classList.add('visible');
|
|
67
|
+
popup.setAttribute('aria-hidden', 'false');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function acceptItem(input, index) {
|
|
71
|
+
const item = openState?.items?.[index];
|
|
72
|
+
if (!item || item.insert === null) return false;
|
|
73
|
+
const [start, end] = openState.replaceRange;
|
|
74
|
+
const value = String(input.value || '');
|
|
75
|
+
const inserted = `${item.insert} `;
|
|
76
|
+
input.value = `${value.slice(0, start)}${inserted}${value.slice(end)}`;
|
|
77
|
+
const caret = start + inserted.length;
|
|
78
|
+
input.selectionStart = caret;
|
|
79
|
+
input.selectionEnd = caret;
|
|
80
|
+
input.focus();
|
|
81
|
+
// Re-fires the composer's input pipeline (autoResize + this menu), so
|
|
82
|
+
// accepting a command immediately opens its next completion level.
|
|
83
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Recomputes the menu for the current input state. The composer calls this
|
|
89
|
+
* from the textarea's input handler; anything that is not a completable
|
|
90
|
+
* command head closes the menu.
|
|
91
|
+
*/
|
|
92
|
+
export function updateSlashAutocomplete(input, { conversationId = '' } = {}) {
|
|
93
|
+
if (!input) return;
|
|
94
|
+
const text = String(input.value || '');
|
|
95
|
+
if (!text.startsWith('/')) {
|
|
96
|
+
closeSlashAutocomplete();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const caret = Number.isFinite(input.selectionStart) ? input.selectionStart : text.length;
|
|
100
|
+
const next = completionsFor(text, caret, { previews: getPreviews(), conversationId });
|
|
101
|
+
if (!next) {
|
|
102
|
+
closeSlashAutocomplete();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// A rebuild replaces the item set, so a stale highlight index must not
|
|
106
|
+
// survive onto a different row.
|
|
107
|
+
const previousSelected = selectedIndex >= 0 ? openState?.items?.[selectedIndex] : null;
|
|
108
|
+
openState = next;
|
|
109
|
+
selectedIndex = previousSelected
|
|
110
|
+
? next.items.findIndex((item) => item.insert !== null && item.insert === previousSelected.insert)
|
|
111
|
+
: -1;
|
|
112
|
+
renderMenu();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Offers a keydown to the menu. Returns true when the event was consumed; the
|
|
117
|
+
* composer's own handleKey must call this first and stop on true.
|
|
118
|
+
*/
|
|
119
|
+
export function handleSlashAutocompleteKey(event, input) {
|
|
120
|
+
if (!openState) return false;
|
|
121
|
+
|
|
122
|
+
if (event.key === 'Escape') {
|
|
123
|
+
closeSlashAutocomplete();
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
// Swallowed entirely so the document-level Escape handlers (emoji picker,
|
|
126
|
+
// modals) do not also fire off one keypress.
|
|
127
|
+
event.stopPropagation();
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
132
|
+
const selectable = insertableIndexes();
|
|
133
|
+
if (!selectable.length) return false;
|
|
134
|
+
const position = selectable.indexOf(selectedIndex);
|
|
135
|
+
if (event.key === 'ArrowDown') {
|
|
136
|
+
selectedIndex = position === selectable.length - 1
|
|
137
|
+
? selectable[0]
|
|
138
|
+
: selectable[position + 1] ?? selectable[0];
|
|
139
|
+
} else {
|
|
140
|
+
// ↑ from the top row returns to the no-highlight state, keeping plain
|
|
141
|
+
// Enter's newline reachable without Escape.
|
|
142
|
+
selectedIndex = position <= 0 ? -1 : selectable[position - 1];
|
|
143
|
+
}
|
|
144
|
+
renderMenu();
|
|
145
|
+
event.preventDefault();
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (event.key === 'Tab') {
|
|
150
|
+
const selectable = insertableIndexes();
|
|
151
|
+
if (!selectable.length) return false;
|
|
152
|
+
const target = selectedIndex >= 0 ? selectedIndex : selectable[0];
|
|
153
|
+
acceptItem(input, target);
|
|
154
|
+
event.preventDefault();
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (event.key === 'Enter' && !event.ctrlKey && !event.metaKey) {
|
|
159
|
+
if (selectedIndex < 0) return false;
|
|
160
|
+
acceptItem(input, selectedIndex);
|
|
161
|
+
event.preventDefault();
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function initSlashAutocomplete() {
|
|
169
|
+
const popup = popupEl();
|
|
170
|
+
if (!popup || popup.dataset.bound === '1') return;
|
|
171
|
+
popup.dataset.bound = '1';
|
|
172
|
+
// pointerdown, not click: the textarea loses focus on the tap, and a click
|
|
173
|
+
// handler would race the focus-driven close.
|
|
174
|
+
popup.addEventListener('pointerdown', (event) => {
|
|
175
|
+
const row = event.target?.closest?.('.slash-item');
|
|
176
|
+
if (!row || !openState) return;
|
|
177
|
+
event.preventDefault();
|
|
178
|
+
const input = document.getElementById('msg-input');
|
|
179
|
+
if (input) acceptItem(input, Number(row.dataset.index));
|
|
180
|
+
});
|
|
181
|
+
document.addEventListener('pointerdown', (event) => {
|
|
182
|
+
if (!openState) return;
|
|
183
|
+
const target = event.target;
|
|
184
|
+
if (popupEl()?.contains(target)) return;
|
|
185
|
+
if (document.getElementById('msg-input')?.contains?.(target)) return;
|
|
186
|
+
closeSlashAutocomplete();
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// The composer's slash-command registry: the one place a command declares its
|
|
2
|
+
// name, description, and completion structure. DOM-free by design — the menu
|
|
3
|
+
// (slash-autocomplete.mjs) renders whatever completionsFor() returns, and the
|
|
4
|
+
// typo guard in sendMessage() asks matchesKnownCommand(). Adding a command is
|
|
5
|
+
// one entry here plus its intercept in sendMessage.
|
|
6
|
+
|
|
7
|
+
export const SLASH_COMMANDS = [
|
|
8
|
+
{
|
|
9
|
+
name: 'compact',
|
|
10
|
+
description: 'Branch to a fresh conversation seeded with summary context',
|
|
11
|
+
usage: '/compact',
|
|
12
|
+
subcommands: [],
|
|
13
|
+
argHints: [],
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'preview',
|
|
17
|
+
description: 'Publish a port or folder on the public preview URL',
|
|
18
|
+
usage: '/preview <port|dir> [label]',
|
|
19
|
+
subcommands: [
|
|
20
|
+
{ name: 'list', description: 'List live previews' },
|
|
21
|
+
{ name: 'close', description: 'Close a preview', dynamicArgs: 'previews' },
|
|
22
|
+
],
|
|
23
|
+
argHints: [
|
|
24
|
+
{ hint: '<port>', description: 'proxy an already-running dev server' },
|
|
25
|
+
{ hint: '<dir>', description: 'serve a folder statically' },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function commandByName(name) {
|
|
31
|
+
const lower = String(name || '').toLowerCase();
|
|
32
|
+
return SLASH_COMMANDS.find((command) => command.name === lower) || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* True when the first token would actually parse as a registered command.
|
|
37
|
+
* Exact name only: the guard runs at send time, where a prefix the user never
|
|
38
|
+
* completed is precisely the typo being guarded against.
|
|
39
|
+
*/
|
|
40
|
+
export function matchesKnownCommand(text) {
|
|
41
|
+
const trimmed = String(text || '');
|
|
42
|
+
if (!trimmed.startsWith('/')) return false;
|
|
43
|
+
const first = trimmed.slice(1).split(/\s+/, 1)[0] || '';
|
|
44
|
+
return commandByName(first) !== null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const UNKNOWN_COMMAND_REPEAT_WINDOW_MS = 8000;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Warn-once decision for the unknown-command send guard. Pure: the composer
|
|
51
|
+
* holds the slot and passes it back on the next attempt. Returns
|
|
52
|
+
* `{ warn, notice?, slot }` — warn=true blocks the send and stores `slot`;
|
|
53
|
+
* warn=false lets it through (and clears the slot).
|
|
54
|
+
*
|
|
55
|
+
* Only single-line, attachment-free text that starts with "/" and parses as no
|
|
56
|
+
* known command is guarded: multi-line text is clearly prose, and an
|
|
57
|
+
* attachment send was never a command.
|
|
58
|
+
*/
|
|
59
|
+
export function evaluateUnknownCommandGuard(text, { slot = null, now = Date.now(), hasAttachments = false } = {}) {
|
|
60
|
+
const value = String(text || '');
|
|
61
|
+
if (hasAttachments || !value.startsWith('/') || value.includes('\n') || matchesKnownCommand(value)) {
|
|
62
|
+
return { warn: false, slot: null };
|
|
63
|
+
}
|
|
64
|
+
if (slot?.text === value && now - slot.at <= UNKNOWN_COMMAND_REPEAT_WINDOW_MS) {
|
|
65
|
+
return { warn: false, slot: null };
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
warn: true,
|
|
69
|
+
notice: `Unknown command ${value.split(/\s+/, 1)[0]} — press send again to send as text.`,
|
|
70
|
+
slot: { text: value, at: now },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function commandItems(prefix) {
|
|
75
|
+
const lower = prefix.toLowerCase();
|
|
76
|
+
return SLASH_COMMANDS
|
|
77
|
+
.filter((command) => command.name.startsWith(lower))
|
|
78
|
+
.map((command) => ({
|
|
79
|
+
kind: 'command',
|
|
80
|
+
insert: `/${command.name}`,
|
|
81
|
+
display: `/${command.name}`,
|
|
82
|
+
description: command.description,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function subcommandItems(command, prefix, previews, conversationId) {
|
|
87
|
+
const lower = prefix.toLowerCase();
|
|
88
|
+
const items = command.subcommands
|
|
89
|
+
.filter((sub) => sub.name.startsWith(lower))
|
|
90
|
+
.map((sub) => ({
|
|
91
|
+
kind: 'subcommand',
|
|
92
|
+
insert: sub.name,
|
|
93
|
+
display: sub.name,
|
|
94
|
+
description: sub.description,
|
|
95
|
+
}));
|
|
96
|
+
// Hint rows only while nothing has been typed yet: once the user starts a
|
|
97
|
+
// token they have chosen a shape, and a non-insertable row would sit in the
|
|
98
|
+
// way of Tab-accepting a real match.
|
|
99
|
+
if (!prefix) {
|
|
100
|
+
for (const hint of command.argHints) {
|
|
101
|
+
items.push({
|
|
102
|
+
kind: 'hint',
|
|
103
|
+
insert: null,
|
|
104
|
+
display: hint.hint,
|
|
105
|
+
description: hint.description,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return items;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function previewTokenItems(prefix, previews, conversationId) {
|
|
113
|
+
const lower = prefix.toLowerCase();
|
|
114
|
+
const entries = (Array.isArray(previews) ? previews : [])
|
|
115
|
+
.filter((entry) => entry && typeof entry.token === 'string')
|
|
116
|
+
.filter((entry) => entry.token.startsWith(lower));
|
|
117
|
+
const convId = String(conversationId || '').trim();
|
|
118
|
+
// This conversation's previews first — closing what you just made is the
|
|
119
|
+
// common case; foreign ones follow with their owning session shown.
|
|
120
|
+
const mine = entries.filter((entry) => convId && entry.conversationId === convId);
|
|
121
|
+
const others = entries.filter((entry) => !(convId && entry.conversationId === convId));
|
|
122
|
+
return [...mine, ...others].map((entry) => ({
|
|
123
|
+
kind: 'preview-token',
|
|
124
|
+
insert: entry.token,
|
|
125
|
+
display: entry.token.slice(0, 8),
|
|
126
|
+
description: mine.includes(entry) || !entry.conversationId
|
|
127
|
+
? String(entry.label || '')
|
|
128
|
+
: `${entry.label || ''} · session ${String(entry.conversationId).slice(0, 8)}`,
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Completions for the caret position, or null when the menu must close.
|
|
134
|
+
*
|
|
135
|
+
* Only the message head is completed: the command grammar lives entirely
|
|
136
|
+
* before the free-text label, so completion stops once the caret moves past
|
|
137
|
+
* the token the grammar can still predict. `replaceRange` is [start, end) in
|
|
138
|
+
* the input string; accepting a completion replaces that range with
|
|
139
|
+
* `item.insert` plus a trailing space.
|
|
140
|
+
*/
|
|
141
|
+
export function completionsFor(text, caret, { previews = [], conversationId = '' } = {}) {
|
|
142
|
+
const value = String(text || '');
|
|
143
|
+
const position = Number.isFinite(caret) ? caret : value.length;
|
|
144
|
+
// Position-0 gating: a slash anywhere else is a path, never a command.
|
|
145
|
+
if (!value.startsWith('/')) return null;
|
|
146
|
+
// Completion only makes sense while the caret is in the head of the message.
|
|
147
|
+
if (value.slice(0, position).includes('\n')) return null;
|
|
148
|
+
|
|
149
|
+
const head = value.slice(0, position);
|
|
150
|
+
const afterSlash = head.slice(1);
|
|
151
|
+
const tokens = afterSlash.split(/\s+/);
|
|
152
|
+
|
|
153
|
+
// Still typing the command name (no space yet).
|
|
154
|
+
if (tokens.length === 1) {
|
|
155
|
+
const items = commandItems(tokens[0]);
|
|
156
|
+
if (!items.length) return null;
|
|
157
|
+
return { items, replaceRange: [0, position] };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const command = commandByName(tokens[0]);
|
|
161
|
+
if (!command) return null;
|
|
162
|
+
|
|
163
|
+
const tokenStart = head.length - tokens[tokens.length - 1].length;
|
|
164
|
+
|
|
165
|
+
// Typing the second token: subcommands + shape hints.
|
|
166
|
+
if (tokens.length === 2) {
|
|
167
|
+
const items = subcommandItems(command, tokens[1], previews, conversationId);
|
|
168
|
+
if (!items.length) return null;
|
|
169
|
+
return { items, replaceRange: [tokenStart, position] };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Third token of "/preview close ": live preview tokens.
|
|
173
|
+
if (tokens.length === 3) {
|
|
174
|
+
const sub = command.subcommands.find((entry) => entry.name === tokens[1].toLowerCase());
|
|
175
|
+
if (sub?.dynamicArgs === 'previews') {
|
|
176
|
+
const items = previewTokenItems(tokens[2], previews, conversationId);
|
|
177
|
+
if (!items.length) return null;
|
|
178
|
+
return { items, replaceRange: [tokenStart, position] };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return null;
|
|
183
|
+
}
|