@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,1454 @@
|
|
|
1
|
+
import { recordCliLifecycleEvent, recordRelayLifecycleEvent } from './status-store.mjs';
|
|
2
|
+
import { resolveRelayDotState } from './relay-dot-state.mjs';
|
|
3
|
+
import { readRepoBrowserPreferences } from './repo-browser-preferences.mjs';
|
|
4
|
+
import { isChatInteractionHeld } from './selection-guard.mjs';
|
|
5
|
+
|
|
6
|
+
function resolveAppBase() {
|
|
7
|
+
const configuredBase = typeof window.__COPILOT_APP_CONFIG?.basePath === 'string'
|
|
8
|
+
? window.__COPILOT_APP_CONFIG.basePath.trim()
|
|
9
|
+
: '';
|
|
10
|
+
if (configuredBase && configuredBase !== '/') {
|
|
11
|
+
return configuredBase.startsWith('/') ? configuredBase.replace(/\/+$/, '') : `/${configuredBase.replace(/\/+$/, '')}`;
|
|
12
|
+
}
|
|
13
|
+
const pathname = String(window.location.pathname || '/');
|
|
14
|
+
const sharedIndex = pathname.indexOf('/shared/');
|
|
15
|
+
if (sharedIndex >= 0) return pathname.slice(0, sharedIndex).replace(/\/+$/, '');
|
|
16
|
+
return pathname.replace(/\/+$/, '');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const BASE = resolveAppBase();
|
|
20
|
+
function extractSharedTokenFromPathname(pathname) {
|
|
21
|
+
const path = String(pathname || '/');
|
|
22
|
+
const match = path.match(/\/shared\/([^/?#]+)\/?$/i);
|
|
23
|
+
if (!match) return '';
|
|
24
|
+
return String(match[1] || '').trim().toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveSharedConversationToken() {
|
|
28
|
+
const configured = String(window.__COPILOT_APP_CONFIG?.sharedToken || '').trim().toLowerCase();
|
|
29
|
+
if (configured) return configured;
|
|
30
|
+
return extractSharedTokenFromPathname(window.location.pathname);
|
|
31
|
+
}
|
|
32
|
+
export const SHARED_CONVERSATION_TOKEN = resolveSharedConversationToken();
|
|
33
|
+
export const IS_SHARED_VIEW = !!SHARED_CONVERSATION_TOKEN;
|
|
34
|
+
export let TOKEN = '';
|
|
35
|
+
export let socket = null;
|
|
36
|
+
export let currentConvId = null;
|
|
37
|
+
let clientIdStorageValue = '';
|
|
38
|
+
try {
|
|
39
|
+
clientIdStorageValue = sessionStorage.getItem('copilot_client_id') || '';
|
|
40
|
+
} catch {
|
|
41
|
+
clientIdStorageValue = '';
|
|
42
|
+
}
|
|
43
|
+
export let CLIENT_ID = clientIdStorageValue;
|
|
44
|
+
if (!CLIENT_ID) {
|
|
45
|
+
CLIENT_ID = generateId();
|
|
46
|
+
try {
|
|
47
|
+
sessionStorage.setItem('copilot_client_id', CLIENT_ID);
|
|
48
|
+
} catch {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Durable device identity for push subscriptions and visibility heartbeats.
|
|
52
|
+
// Unlike CLIENT_ID (per-tab, sessionStorage) this survives restarts so a
|
|
53
|
+
// device keeps its identity across subscription churn.
|
|
54
|
+
let deviceIdStorageValue = '';
|
|
55
|
+
try {
|
|
56
|
+
deviceIdStorageValue = localStorage.getItem('copilot_device_id') || '';
|
|
57
|
+
} catch {
|
|
58
|
+
deviceIdStorageValue = '';
|
|
59
|
+
}
|
|
60
|
+
export let DEVICE_ID = deviceIdStorageValue;
|
|
61
|
+
if (!DEVICE_ID) {
|
|
62
|
+
DEVICE_ID = generateId();
|
|
63
|
+
try {
|
|
64
|
+
localStorage.setItem('copilot_device_id', DEVICE_ID);
|
|
65
|
+
} catch {}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const seenMessageIds = new Set();
|
|
69
|
+
export const pendingUserMessageIds = new Set();
|
|
70
|
+
export const pendingUserMessageEntries = new Map();
|
|
71
|
+
export let cliOnline = false;
|
|
72
|
+
export let relayOnline = false;
|
|
73
|
+
// Latest cloudflared tunnel status, from /api/status on load and the
|
|
74
|
+
// cloudflared_tunnel_status socket event thereafter. Null until first reported.
|
|
75
|
+
export let cloudflaredTunnelState = null;
|
|
76
|
+
export let activeRuntimeSessionCount = 0;
|
|
77
|
+
export let runtimeSessionBindingCount = 0;
|
|
78
|
+
export let conversations = {};
|
|
79
|
+
export let selectedAttachments = [];
|
|
80
|
+
export let imageEditTarget = null;
|
|
81
|
+
export const RELAY_QUESTION_POLL_MS = 3000;
|
|
82
|
+
export const MAX_UPLOAD_ATTACHMENTS = 6;
|
|
83
|
+
export const FILE_PREVIEW_MAX_BYTES = 512 * 1024;
|
|
84
|
+
export const REPO_IMAGE_EXTENSIONS = new Set(['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'ico', 'bmp', 'avif']);
|
|
85
|
+
export const WORKSPACE_FILE_EXTENSIONS = new Set([
|
|
86
|
+
'md', 'txt', 'json', 'js', 'mjs', 'cjs', 'ts', 'tsx', 'jsx', 'css', 'html', 'xml', 'yml', 'yaml',
|
|
87
|
+
'toml', 'ini', 'csv', 'sql', 'ps1', 'sh', 'bat', 'go', 'py', 'java', 'rb', 'php', 'c', 'h', 'cpp',
|
|
88
|
+
'hpp', 'rs', 'lock', 'log', 'svg', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'ico', 'pdf',
|
|
89
|
+
]);
|
|
90
|
+
export let serverPlatform = '';
|
|
91
|
+
export let workspaceRootName = '';
|
|
92
|
+
export let workspaceRootPath = '';
|
|
93
|
+
export let defaultSessionWorkspaceRootPath = '';
|
|
94
|
+
export let defaultSessionWorkspaceRootWarning = '';
|
|
95
|
+
export let workspaceRootEntrySet = new Set([
|
|
96
|
+
'.github',
|
|
97
|
+
'server',
|
|
98
|
+
'tests',
|
|
99
|
+
'readme.md',
|
|
100
|
+
'package.json',
|
|
101
|
+
'package-lock.json',
|
|
102
|
+
]);
|
|
103
|
+
export let recentWorkspaceRoots = [];
|
|
104
|
+
export let relayQuestions = new Map();
|
|
105
|
+
export let relayBoards = new Map();
|
|
106
|
+
export let relayActivities = new Map();
|
|
107
|
+
export let relayThoughts = new Map();
|
|
108
|
+
export let conversationWatcherCounts = new Map();
|
|
109
|
+
export let sessionWorkerStates = new Map();
|
|
110
|
+
export let subagentRuns = new Map();
|
|
111
|
+
export const subagentCancelInFlight = new Set();
|
|
112
|
+
export let thinkingMessageId = null;
|
|
113
|
+
export let relayQuestionPollTimer = null;
|
|
114
|
+
export let relayQuestionRenderHash = '';
|
|
115
|
+
export let relayQuestionDrafts = new Map();
|
|
116
|
+
export let selectedAttachmentsState = selectedAttachments;
|
|
117
|
+
export let filePreviewState = {
|
|
118
|
+
path: '',
|
|
119
|
+
source: 'workspace',
|
|
120
|
+
mode: 'preview',
|
|
121
|
+
allowHtml: true,
|
|
122
|
+
loading: false,
|
|
123
|
+
error: '',
|
|
124
|
+
payload: null,
|
|
125
|
+
viewerOptions: {
|
|
126
|
+
startSeconds: 0,
|
|
127
|
+
preload: 'metadata',
|
|
128
|
+
autoplay: false,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
// Toolbar filters are sticky per browser (all default off); the toggles in
|
|
132
|
+
// attachments-view.js are the only writers.
|
|
133
|
+
const repoBrowserPreferences = readRepoBrowserPreferences();
|
|
134
|
+
export let repoBrowserState = {
|
|
135
|
+
open: false,
|
|
136
|
+
loading: false,
|
|
137
|
+
activeRoot: 'workspace',
|
|
138
|
+
workspaceIncludeHidden: repoBrowserPreferences.workspaceIncludeHidden,
|
|
139
|
+
workspaceIncludeHeavy: repoBrowserPreferences.workspaceIncludeHeavy,
|
|
140
|
+
drivesIncludeHidden: repoBrowserPreferences.drivesIncludeHidden,
|
|
141
|
+
viewMode: 'list',
|
|
142
|
+
rootName: 'repo',
|
|
143
|
+
sessionRootPath: '',
|
|
144
|
+
sessionRootName: 'Session',
|
|
145
|
+
tree: null,
|
|
146
|
+
nodeMap: new Map(),
|
|
147
|
+
expandedPaths: new Set(),
|
|
148
|
+
collapsedPaths: new Set(),
|
|
149
|
+
currentPath: '',
|
|
150
|
+
truncated: false,
|
|
151
|
+
nodeCount: 0,
|
|
152
|
+
maxNodes: 0,
|
|
153
|
+
loadingPath: '',
|
|
154
|
+
error: '',
|
|
155
|
+
};
|
|
156
|
+
export let contextIndicatorMode = 'bar';
|
|
157
|
+
export let compactInFlight = false;
|
|
158
|
+
export let deferredInstallPrompt = null;
|
|
159
|
+
export let viewportBaseHeight = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
160
|
+
export let historyRefreshInFlight = false;
|
|
161
|
+
const SIDEBAR_WIDTH_PERCENT_MIN = 10;
|
|
162
|
+
const SIDEBAR_WIDTH_PERCENT_MAX = 50;
|
|
163
|
+
const SIDEBAR_WIDTH_PERCENT_FALLBACK = 24;
|
|
164
|
+
const SIDEBAR_WIDTH_PORTRAIT_STORAGE_KEY = 'copilot_sidebar_width_pct_portrait';
|
|
165
|
+
const SIDEBAR_WIDTH_LANDSCAPE_STORAGE_KEY = 'copilot_sidebar_width_pct_landscape';
|
|
166
|
+
const SIDEBAR_COLLAPSED_DESKTOP_STORAGE_KEY = 'copilot_sidebar_collapsed_desktop';
|
|
167
|
+
let sidebarWidthPercent = SIDEBAR_WIDTH_PERCENT_FALLBACK;
|
|
168
|
+
const CONVERSATION_SCROLL_STORAGE_PREFIX = 'copilot_message_scroll_';
|
|
169
|
+
|
|
170
|
+
if (globalThis.marked && typeof globalThis.marked.setOptions === 'function') {
|
|
171
|
+
globalThis.marked.setOptions({ breaks: true });
|
|
172
|
+
} else {
|
|
173
|
+
console.warn('[store] marked unavailable; markdown rendering will use plain-text fallback.');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function generateId() {
|
|
177
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) return crypto.randomUUID();
|
|
178
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
|
|
179
|
+
const r = Math.random() * 16 | 0;
|
|
180
|
+
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function setCurrentConv(id) {
|
|
185
|
+
currentConvId = id;
|
|
186
|
+
if (id) localStorage.setItem('copilot_last_conv', id);
|
|
187
|
+
else localStorage.removeItem('copilot_last_conv');
|
|
188
|
+
updateCompactButton();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function setConversationWatcherCount(conversationId, count) {
|
|
192
|
+
const id = String(conversationId || '').trim();
|
|
193
|
+
if (!id) return;
|
|
194
|
+
const next = Number.isFinite(Number(count)) ? Math.max(0, Math.trunc(Number(count))) : 0;
|
|
195
|
+
if (next <= 0) {
|
|
196
|
+
conversationWatcherCounts.delete(id);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
conversationWatcherCounts.set(id, next);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function getConversationWatcherCount(conversationId) {
|
|
203
|
+
const id = String(conversationId || '').trim();
|
|
204
|
+
if (!id) return 0;
|
|
205
|
+
return Number(conversationWatcherCounts.get(id) || 0);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function conversationScrollStorageKey(conversationId) {
|
|
209
|
+
const id = String(conversationId || '').trim();
|
|
210
|
+
return id ? `${CONVERSATION_SCROLL_STORAGE_PREFIX}${id}` : '';
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function readConversationViewState(conversationId) {
|
|
214
|
+
const key = conversationScrollStorageKey(conversationId);
|
|
215
|
+
if (!key) return null;
|
|
216
|
+
const raw = localStorage.getItem(key);
|
|
217
|
+
if (raw === null) return null;
|
|
218
|
+
const trimmed = String(raw || '').trim();
|
|
219
|
+
if (!trimmed) return null;
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(trimmed);
|
|
222
|
+
if (parsed && typeof parsed === 'object') {
|
|
223
|
+
return {
|
|
224
|
+
scrollTop: Number.isFinite(Number(parsed.scrollTop)) && Number(parsed.scrollTop) >= 0
|
|
225
|
+
? Math.trunc(Number(parsed.scrollTop))
|
|
226
|
+
: null,
|
|
227
|
+
loadedMessageCount: Number.isFinite(Number(parsed.loadedMessageCount)) && Number(parsed.loadedMessageCount) >= 0
|
|
228
|
+
? Math.trunc(Number(parsed.loadedMessageCount))
|
|
229
|
+
: null,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
// Legacy numeric storage falls through below.
|
|
234
|
+
}
|
|
235
|
+
const legacyScrollTop = Number(trimmed);
|
|
236
|
+
if (!Number.isFinite(legacyScrollTop) || legacyScrollTop < 0) return null;
|
|
237
|
+
return {
|
|
238
|
+
scrollTop: Math.trunc(legacyScrollTop),
|
|
239
|
+
loadedMessageCount: null,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function writeConversationViewState(conversationId, nextState = {}) {
|
|
244
|
+
const key = conversationScrollStorageKey(conversationId);
|
|
245
|
+
if (!key) return false;
|
|
246
|
+
const current = readConversationViewState(conversationId) || {};
|
|
247
|
+
const next = {
|
|
248
|
+
scrollTop: Number.isFinite(Number(nextState.scrollTop)) && Number(nextState.scrollTop) >= 0
|
|
249
|
+
? Math.trunc(Number(nextState.scrollTop))
|
|
250
|
+
: current.scrollTop ?? null,
|
|
251
|
+
loadedMessageCount: Number.isFinite(Number(nextState.loadedMessageCount)) && Number(nextState.loadedMessageCount) >= 0
|
|
252
|
+
? Math.trunc(Number(nextState.loadedMessageCount))
|
|
253
|
+
: current.loadedMessageCount ?? null,
|
|
254
|
+
};
|
|
255
|
+
localStorage.setItem(key, JSON.stringify(next));
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export function saveConversationScrollTop(conversationId, scrollTop) {
|
|
260
|
+
const value = Number(scrollTop);
|
|
261
|
+
if (!Number.isFinite(value) || value < 0) return false;
|
|
262
|
+
try {
|
|
263
|
+
window.__scrollSaveDebug = {
|
|
264
|
+
conversationId: String(conversationId || '').trim() || null,
|
|
265
|
+
scrollTop: Math.trunc(value),
|
|
266
|
+
at: Date.now(),
|
|
267
|
+
stack: String(new Error().stack || '')
|
|
268
|
+
.split('\n')
|
|
269
|
+
.slice(1, 4)
|
|
270
|
+
.map((line) => line.trim()),
|
|
271
|
+
};
|
|
272
|
+
} catch {}
|
|
273
|
+
return writeConversationViewState(conversationId, { scrollTop: value });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function saveConversationLoadedMessageCount(conversationId, loadedMessageCount) {
|
|
277
|
+
const value = Number(loadedMessageCount);
|
|
278
|
+
if (!Number.isFinite(value) || value < 0) return false;
|
|
279
|
+
return writeConversationViewState(conversationId, { loadedMessageCount: value });
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function loadConversationScrollTop(conversationId) {
|
|
283
|
+
return readConversationViewState(conversationId)?.scrollTop ?? null;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function loadConversationLoadedMessageCount(conversationId) {
|
|
287
|
+
return readConversationViewState(conversationId)?.loadedMessageCount ?? null;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function normalizePendingMessageText(text) {
|
|
291
|
+
return String(text || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function pendingConversationKey(conversationId) {
|
|
295
|
+
return String(conversationId || '').trim() || '__new__';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function cleanupStalePendingUserMessages(maxAgeMs = 15 * 60 * 1000) {
|
|
299
|
+
const cutoff = Date.now() - Math.max(60_000, Number(maxAgeMs) || 0);
|
|
300
|
+
for (const [messageId, entry] of pendingUserMessageEntries.entries()) {
|
|
301
|
+
const addedAt = Number(entry?.addedAt || 0);
|
|
302
|
+
if (!Number.isFinite(addedAt) || addedAt < cutoff) {
|
|
303
|
+
pendingUserMessageEntries.delete(messageId);
|
|
304
|
+
pendingUserMessageIds.delete(messageId);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function trackPendingUserMessage(messageId, conversationId, text) {
|
|
310
|
+
const id = String(messageId || '').trim();
|
|
311
|
+
const fingerprint = normalizePendingMessageText(text);
|
|
312
|
+
if (!id || !fingerprint) return false;
|
|
313
|
+
cleanupStalePendingUserMessages();
|
|
314
|
+
pendingUserMessageEntries.set(id, {
|
|
315
|
+
conversationKey: pendingConversationKey(conversationId),
|
|
316
|
+
fingerprint,
|
|
317
|
+
addedAt: Date.now(),
|
|
318
|
+
});
|
|
319
|
+
pendingUserMessageIds.add(id);
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function clearPendingUserMessage(messageId) {
|
|
324
|
+
const id = String(messageId || '').trim();
|
|
325
|
+
if (!id) return false;
|
|
326
|
+
pendingUserMessageIds.delete(id);
|
|
327
|
+
return pendingUserMessageEntries.delete(id);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function hasPendingUserMessageDuplicate(conversationId, text) {
|
|
331
|
+
const fingerprint = normalizePendingMessageText(text);
|
|
332
|
+
if (!fingerprint) return false;
|
|
333
|
+
cleanupStalePendingUserMessages();
|
|
334
|
+
const conversationKey = pendingConversationKey(conversationId);
|
|
335
|
+
for (const entry of pendingUserMessageEntries.values()) {
|
|
336
|
+
if (entry?.conversationKey === conversationKey && entry?.fingerprint === fingerprint) {
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function authHeaders() {
|
|
344
|
+
return TOKEN ? { Authorization: `Bearer ${TOKEN}` } : {};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function setToken(value) {
|
|
348
|
+
TOKEN = String(value || '').trim();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function escHtml(s) {
|
|
352
|
+
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function formatBytes(bytes) {
|
|
356
|
+
const value = Number(bytes || 0);
|
|
357
|
+
if (!Number.isFinite(value) || value <= 0) return '0 B';
|
|
358
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
359
|
+
let size = value;
|
|
360
|
+
let idx = 0;
|
|
361
|
+
while (size >= 1024 && idx < units.length - 1) {
|
|
362
|
+
size /= 1024;
|
|
363
|
+
idx += 1;
|
|
364
|
+
}
|
|
365
|
+
const rounded = idx === 0 ? Math.round(size) : Math.round(size * 10) / 10;
|
|
366
|
+
return `${rounded} ${units[idx]}`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function fmtDate(iso) {
|
|
370
|
+
if (!iso) return '';
|
|
371
|
+
const timestampMs = parseTimestampMs(iso);
|
|
372
|
+
if (!timestampMs) return '';
|
|
373
|
+
const d = new Date(timestampMs);
|
|
374
|
+
const now = new Date();
|
|
375
|
+
if (d.toDateString() === now.toDateString()) {
|
|
376
|
+
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
377
|
+
}
|
|
378
|
+
return `${d.toLocaleDateString([], { month: 'short', day: 'numeric' })} ${d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function parseTimestampMs(value) {
|
|
382
|
+
const text = String(value || '').trim();
|
|
383
|
+
if (!text) return 0;
|
|
384
|
+
const sqliteUtcLike = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2}(?:\.\d{1,6})?)$/;
|
|
385
|
+
const hasExplicitTimezone = /(?:Z|[+-]\d{2}:?\d{2})$/i.test(text);
|
|
386
|
+
let normalized = text;
|
|
387
|
+
if (!hasExplicitTimezone && sqliteUtcLike.test(text)) {
|
|
388
|
+
const [, day, time] = text.match(sqliteUtcLike) || [];
|
|
389
|
+
normalized = `${day}T${time}Z`;
|
|
390
|
+
}
|
|
391
|
+
const parsed = Date.parse(normalized);
|
|
392
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function setServerPlatform(value) {
|
|
396
|
+
serverPlatform = String(value || '').trim().toLowerCase();
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function updateWorkspaceRootHints(payload) {
|
|
400
|
+
const normalizedPayload = payload && typeof payload === 'object' ? payload : {};
|
|
401
|
+
if (Object.prototype.hasOwnProperty.call(normalizedPayload, 'workspaceRootName')) {
|
|
402
|
+
workspaceRootName = String(normalizedPayload.workspaceRootName || '').trim().toLowerCase();
|
|
403
|
+
}
|
|
404
|
+
if (Object.prototype.hasOwnProperty.call(normalizedPayload, 'workspaceRootPath')) {
|
|
405
|
+
workspaceRootPath = String(normalizedPayload.workspaceRootPath || '').trim();
|
|
406
|
+
}
|
|
407
|
+
if (Object.prototype.hasOwnProperty.call(normalizedPayload, 'defaultSessionWorkspaceRootPath')
|
|
408
|
+
|| Object.prototype.hasOwnProperty.call(normalizedPayload, 'default_session_workspace_root_path')) {
|
|
409
|
+
defaultSessionWorkspaceRootPath = String(
|
|
410
|
+
normalizedPayload.defaultSessionWorkspaceRootPath
|
|
411
|
+
?? normalizedPayload.default_session_workspace_root_path
|
|
412
|
+
?? '',
|
|
413
|
+
).trim();
|
|
414
|
+
}
|
|
415
|
+
if (Object.prototype.hasOwnProperty.call(normalizedPayload, 'defaultSessionWorkspaceRootWarning')
|
|
416
|
+
|| Object.prototype.hasOwnProperty.call(normalizedPayload, 'default_session_workspace_root_warning')) {
|
|
417
|
+
defaultSessionWorkspaceRootWarning = String(
|
|
418
|
+
normalizedPayload.defaultSessionWorkspaceRootWarning
|
|
419
|
+
?? normalizedPayload.default_session_workspace_root_warning
|
|
420
|
+
?? '',
|
|
421
|
+
).trim();
|
|
422
|
+
}
|
|
423
|
+
if (Object.prototype.hasOwnProperty.call(normalizedPayload, 'workspaceRootEntries')) {
|
|
424
|
+
const entries = Array.isArray(normalizedPayload.workspaceRootEntries) ? normalizedPayload.workspaceRootEntries : [];
|
|
425
|
+
workspaceRootEntrySet = new Set(entries.map((entry) => String(entry || '').trim().toLowerCase()).filter(Boolean));
|
|
426
|
+
}
|
|
427
|
+
const recentRoots = Array.isArray(normalizedPayload.recentWorkspaceRoots)
|
|
428
|
+
? normalizedPayload.recentWorkspaceRoots
|
|
429
|
+
: (Array.isArray(normalizedPayload.recentCwds) ? normalizedPayload.recentCwds : null);
|
|
430
|
+
if (Array.isArray(recentRoots)) {
|
|
431
|
+
const deduped = [];
|
|
432
|
+
const seen = new Set();
|
|
433
|
+
for (const candidate of recentRoots) {
|
|
434
|
+
const text = String(candidate || '').trim();
|
|
435
|
+
if (!text) continue;
|
|
436
|
+
const key = text.toLowerCase();
|
|
437
|
+
if (seen.has(key)) continue;
|
|
438
|
+
seen.add(key);
|
|
439
|
+
deduped.push(text);
|
|
440
|
+
}
|
|
441
|
+
recentWorkspaceRoots = deduped;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function getRecentWorkspaceRoots() {
|
|
446
|
+
return recentWorkspaceRoots.slice();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
export function getConversationWorkspaceState(conversationId = currentConvId) {
|
|
450
|
+
const convId = String(conversationId || '').trim();
|
|
451
|
+
if (!convId) return null;
|
|
452
|
+
const conversation = conversations?.[convId] || null;
|
|
453
|
+
if (!conversation) return null;
|
|
454
|
+
const configuredWorkspaceRootPath = String(
|
|
455
|
+
conversation.configuredWorkspaceRootPath || conversation.configured_workspace_root_path || '',
|
|
456
|
+
).trim();
|
|
457
|
+
const configuredWorkspaceRootName = String(
|
|
458
|
+
conversation.configuredWorkspaceRootName || conversation.configured_workspace_root_name || '',
|
|
459
|
+
).trim();
|
|
460
|
+
const runtimeWorkspaceRootPath = String(
|
|
461
|
+
conversation.runtimeWorkspaceRootPath || conversation.runtime_workspace_root_path || '',
|
|
462
|
+
).trim();
|
|
463
|
+
const runtimeWorkspaceRootName = String(
|
|
464
|
+
conversation.runtimeWorkspaceRootName || conversation.runtime_workspace_root_name || '',
|
|
465
|
+
).trim();
|
|
466
|
+
const currentWorkspaceRootPath = String(
|
|
467
|
+
conversation.currentWorkspaceRootPath || conversation.current_workspace_root_path || runtimeWorkspaceRootPath || configuredWorkspaceRootPath || '',
|
|
468
|
+
).trim();
|
|
469
|
+
const currentWorkspaceRootName = String(
|
|
470
|
+
conversation.currentWorkspaceRootName || conversation.current_workspace_root_name || runtimeWorkspaceRootName || configuredWorkspaceRootName || '',
|
|
471
|
+
).trim();
|
|
472
|
+
return {
|
|
473
|
+
configuredWorkspaceRootPath,
|
|
474
|
+
configuredWorkspaceRootName,
|
|
475
|
+
runtimeWorkspaceRootPath,
|
|
476
|
+
runtimeWorkspaceRootName,
|
|
477
|
+
currentWorkspaceRootPath,
|
|
478
|
+
currentWorkspaceRootName,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function getConversationCurrentWorkspaceRootPath(conversationId = currentConvId) {
|
|
483
|
+
return String(getConversationWorkspaceState(conversationId)?.currentWorkspaceRootPath || '').trim();
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export function setActiveRuntimeSessionCount(value) {
|
|
487
|
+
const numeric = Number(value);
|
|
488
|
+
activeRuntimeSessionCount = Number.isFinite(numeric) && numeric > 0 ? Math.trunc(numeric) : 0;
|
|
489
|
+
updateCliStatus();
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
export function setRuntimeSessionBindingCount(value) {
|
|
493
|
+
const numeric = Number(value);
|
|
494
|
+
runtimeSessionBindingCount = Number.isFinite(numeric) && numeric > 0 ? Math.trunc(numeric) : 0;
|
|
495
|
+
updateCliStatus();
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function clampContextUsageRatio(value) {
|
|
499
|
+
const n = Number(value);
|
|
500
|
+
if (!Number.isFinite(n)) return null;
|
|
501
|
+
if (n <= 0) return 0;
|
|
502
|
+
if (n >= 1) return 1;
|
|
503
|
+
return n;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export function readContextUsageRatio(payload) {
|
|
507
|
+
// The unified view payload is authoritative and already clamped; it is the
|
|
508
|
+
// only source Claude sessions have. Snapshot fields stay as the fallback.
|
|
509
|
+
const usagePct = Number(payload?.contextUsage?.percentage);
|
|
510
|
+
if (Number.isFinite(usagePct)) return clampContextUsageRatio(usagePct / 100);
|
|
511
|
+
|
|
512
|
+
const snapshot = payload?.snapshot && typeof payload.snapshot === 'object' ? payload.snapshot : null;
|
|
513
|
+
if (!snapshot) return null;
|
|
514
|
+
const usedPct = Number(snapshot.used_percent);
|
|
515
|
+
if (Number.isFinite(usedPct)) {
|
|
516
|
+
return clampContextUsageRatio(usedPct / 100);
|
|
517
|
+
}
|
|
518
|
+
const usedTotal = Number(snapshot.used_total_tokens);
|
|
519
|
+
const maxTokens = Number(snapshot.max_context_tokens);
|
|
520
|
+
if (Number.isFinite(usedTotal) && Number.isFinite(maxTokens) && maxTokens > 0) {
|
|
521
|
+
return clampContextUsageRatio(usedTotal / maxTokens);
|
|
522
|
+
}
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function normalizeContextIndicatorMode(value) {
|
|
527
|
+
return String(value || '').trim().toLowerCase() === 'bar' ? 'bar' : 'default';
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function setContextIndicatorMode(mode) {
|
|
531
|
+
contextIndicatorMode = normalizeContextIndicatorMode(mode);
|
|
532
|
+
const inputArea = document.getElementById('input-area');
|
|
533
|
+
if (!inputArea) return;
|
|
534
|
+
inputArea.classList.toggle('ctx-indicator-bar', contextIndicatorMode === 'bar');
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function applyContextUsageBar(ratio) {
|
|
538
|
+
const inputArea = document.getElementById('input-area');
|
|
539
|
+
if (!inputArea) return;
|
|
540
|
+
|
|
541
|
+
const normalized = clampContextUsageRatio(ratio);
|
|
542
|
+
if (normalized === null) {
|
|
543
|
+
inputArea.style.setProperty('--context-usage-bar', 'linear-gradient(90deg, rgba(139,148,158,0.65) 0%, rgba(139,148,158,0.95) 50%, rgba(139,148,158,0.65) 100%)');
|
|
544
|
+
inputArea.style.setProperty('--context-usage-glow', 'rgba(139,148,158,0.35)');
|
|
545
|
+
inputArea.style.setProperty('--context-usage-opacity', '0.62');
|
|
546
|
+
inputArea.style.setProperty('--context-usage-ratio', '0');
|
|
547
|
+
inputArea.style.setProperty('--context-usage-hue', '120');
|
|
548
|
+
delete inputArea.dataset.contextUsageRatio;
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
const hue = Math.round((1 - normalized) * 120);
|
|
553
|
+
const lightness = 49 - Math.round(normalized * 8);
|
|
554
|
+
const baseColor = `hsl(${hue}, 88%, ${lightness}%)`;
|
|
555
|
+
const shineAlpha = (0.44 + (normalized * 0.18)).toFixed(2);
|
|
556
|
+
const glowAlpha = Math.min(0.86, 0.42 + (normalized * 0.28)).toFixed(2);
|
|
557
|
+
inputArea.style.setProperty(
|
|
558
|
+
'--context-usage-bar',
|
|
559
|
+
`linear-gradient(90deg, rgba(255,255,255,0.16) 0%, ${baseColor} 24%, rgba(255,255,255,${shineAlpha}) 50%, ${baseColor} 76%, rgba(255,255,255,0.16) 100%)`
|
|
560
|
+
);
|
|
561
|
+
inputArea.style.setProperty('--context-usage-glow', `hsla(${hue}, 88%, 52%, ${glowAlpha})`);
|
|
562
|
+
inputArea.style.setProperty('--context-usage-opacity', '0.98');
|
|
563
|
+
inputArea.style.setProperty('--context-usage-ratio', normalized.toFixed(4));
|
|
564
|
+
inputArea.style.setProperty('--context-usage-hue', String(hue));
|
|
565
|
+
inputArea.dataset.contextUsageRatio = normalized.toFixed(4);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
export function scrollBottom() {
|
|
569
|
+
const el = document.getElementById('messages');
|
|
570
|
+
if (!el) return;
|
|
571
|
+
// Programmatic scrolling during a drag makes the browser extend the live
|
|
572
|
+
// selection over everything that moves under the cursor.
|
|
573
|
+
if (isChatInteractionHeld()) return;
|
|
574
|
+
el.scrollTop = el.scrollHeight;
|
|
575
|
+
saveConversationScrollTop(currentConvId, el.scrollTop);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
export function getMessagesDistanceFromBottom() {
|
|
579
|
+
const el = document.getElementById('messages');
|
|
580
|
+
if (!el) return null;
|
|
581
|
+
return Math.max(0, el.scrollHeight - el.clientHeight - el.scrollTop);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function resolveMessagesNearBottomThreshold(el, thresholdPx = null) {
|
|
585
|
+
if (Number.isFinite(Number(thresholdPx)) && Number(thresholdPx) >= 0) {
|
|
586
|
+
return Math.trunc(Number(thresholdPx));
|
|
587
|
+
}
|
|
588
|
+
const viewportHeight = Number(el?.clientHeight || 0);
|
|
589
|
+
if (!Number.isFinite(viewportHeight) || viewportHeight <= 0) return 0;
|
|
590
|
+
const relativeThreshold = Math.floor(viewportHeight * 0.08);
|
|
591
|
+
return Math.min(48, Math.max(12, relativeThreshold));
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export function isMessagesNearBottom(thresholdPx = null) {
|
|
595
|
+
const el = document.getElementById('messages');
|
|
596
|
+
if (!el) return true;
|
|
597
|
+
const distance = Math.max(0, el.scrollHeight - el.clientHeight - el.scrollTop);
|
|
598
|
+
const threshold = resolveMessagesNearBottomThreshold(el, thresholdPx);
|
|
599
|
+
return distance <= threshold;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
export function isMessagesAtBottom(epsilonPx = 1) {
|
|
603
|
+
const el = document.getElementById('messages');
|
|
604
|
+
if (!el) return true;
|
|
605
|
+
const epsilon = Math.max(0, Number.isFinite(Number(epsilonPx)) ? Number(epsilonPx) : 1);
|
|
606
|
+
const distance = Math.max(0, el.scrollHeight - el.clientHeight - el.scrollTop);
|
|
607
|
+
return distance <= epsilon;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export function scrollBottomAfterSend() {
|
|
611
|
+
scrollBottom();
|
|
612
|
+
requestAnimationFrame(() => {
|
|
613
|
+
scrollBottom();
|
|
614
|
+
});
|
|
615
|
+
setTimeout(() => {
|
|
616
|
+
scrollBottom();
|
|
617
|
+
}, 120);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export function isMobileComposerViewport() {
|
|
621
|
+
return window.matchMedia('(max-width: 680px)').matches;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
export function releaseComposerFocusAfterSend(input) {
|
|
625
|
+
if (!input || !isMobileComposerViewport()) return;
|
|
626
|
+
try { input.blur(); } catch {}
|
|
627
|
+
document.body.classList.remove('keyboard-open');
|
|
628
|
+
syncViewportMetrics();
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
export function autoResize(el) {
|
|
632
|
+
el.style.height = 'auto';
|
|
633
|
+
const maxHeight = Number.parseFloat(globalThis.getComputedStyle?.(el)?.maxHeight || '');
|
|
634
|
+
const cap = Number.isFinite(maxHeight) && maxHeight > 0 ? maxHeight : 160;
|
|
635
|
+
el.style.height = `${Math.min(el.scrollHeight, cap)}px`;
|
|
636
|
+
syncViewportMetrics();
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
export function updateSessionPill(conversation, runtimeSession) {
|
|
640
|
+
const title = document.getElementById('chat-title');
|
|
641
|
+
const usageLine = document.getElementById('chat-title-session-usage');
|
|
642
|
+
const sdkSessionId = String(
|
|
643
|
+
runtimeSession?.sdkSessionId
|
|
644
|
+
|| runtimeSession?.sdk_session_id
|
|
645
|
+
|| conversation?.sdkSessionId
|
|
646
|
+
|| conversation?.sdk_session_id
|
|
647
|
+
|| '',
|
|
648
|
+
).trim();
|
|
649
|
+
if (!sdkSessionId) {
|
|
650
|
+
if (title) {
|
|
651
|
+
delete title.dataset.copilotSessionId;
|
|
652
|
+
title.title = '';
|
|
653
|
+
}
|
|
654
|
+
if (usageLine) {
|
|
655
|
+
usageLine.textContent = '';
|
|
656
|
+
usageLine.hidden = true;
|
|
657
|
+
}
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (title) {
|
|
661
|
+
title.dataset.copilotSessionId = sdkSessionId;
|
|
662
|
+
title.title = `Copilot session ${sdkSessionId} (click to copy)`;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const usageSummary = runtimeSession?.sessionUsageSummary
|
|
666
|
+
|| runtimeSession?.session_usage_summary
|
|
667
|
+
|| conversation?.sessionUsageSummary
|
|
668
|
+
|| conversation?.session_usage_summary
|
|
669
|
+
|| null;
|
|
670
|
+
const aicUsed = Number(usageSummary?.aicUsed ?? usageSummary?.aic_used);
|
|
671
|
+
const premiumRequests = Number(usageSummary?.totalPremiumRequests ?? usageSummary?.total_premium_requests);
|
|
672
|
+
const planCreditsUsed = Number(usageSummary?.planCreditsUsed ?? usageSummary?.plan_credits_used);
|
|
673
|
+
const planMonthlyPercentUsed = Number(usageSummary?.planMonthlyPercentUsed ?? usageSummary?.plan_monthly_percent_used);
|
|
674
|
+
if (!usageLine) return;
|
|
675
|
+
const parts = [];
|
|
676
|
+
if (Number.isFinite(aicUsed) && aicUsed >= 0) {
|
|
677
|
+
parts.push(`Session: ${aicUsed.toFixed(2)} AIC used`);
|
|
678
|
+
} else if (Number.isFinite(planCreditsUsed) && planCreditsUsed > 0) {
|
|
679
|
+
parts.push(`Session plan: ${planCreditsUsed.toFixed(3)} credits used`);
|
|
680
|
+
}
|
|
681
|
+
if (Number.isFinite(planMonthlyPercentUsed) && planMonthlyPercentUsed > 0) {
|
|
682
|
+
parts.push(`${planMonthlyPercentUsed.toFixed(3)}% monthly plan`);
|
|
683
|
+
}
|
|
684
|
+
if (Number.isFinite(premiumRequests) && premiumRequests >= 0) {
|
|
685
|
+
parts.push(`${Math.trunc(premiumRequests)} premium request${Math.trunc(premiumRequests) === 1 ? '' : 's'}`);
|
|
686
|
+
}
|
|
687
|
+
if (!parts.length) {
|
|
688
|
+
usageLine.textContent = '';
|
|
689
|
+
usageLine.hidden = true;
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
usageLine.textContent = parts.join(' · ');
|
|
693
|
+
usageLine.hidden = false;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
export function updateCompactButton() {
|
|
697
|
+
const btn = document.getElementById('chat-menu-compact');
|
|
698
|
+
if (!btn) return;
|
|
699
|
+
if (btn.hidden || btn.getAttribute('aria-hidden') === 'true') {
|
|
700
|
+
btn.disabled = true;
|
|
701
|
+
btn.tabIndex = -1;
|
|
702
|
+
btn.setAttribute('aria-disabled', 'true');
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
const conv = currentConvId ? conversations[currentConvId] : null;
|
|
706
|
+
const canCompact = !!(currentConvId && conv && !conv.archived && !compactInFlight);
|
|
707
|
+
btn.disabled = !canCompact;
|
|
708
|
+
btn.title = canCompact
|
|
709
|
+
? 'Compact conversation into a fresh session'
|
|
710
|
+
: (compactInFlight ? 'Compacting conversation…' : 'Open an active conversation to compact');
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
export function updateCliStatus() {
|
|
714
|
+
const dot = document.getElementById('cli-dot');
|
|
715
|
+
const burger = document.getElementById('sidebar-toggle');
|
|
716
|
+
const text = document.getElementById('cli-status-text');
|
|
717
|
+
const banner = document.getElementById('offline-banner');
|
|
718
|
+
const changeCwdBtn = document.getElementById('chat-menu-change-cwd');
|
|
719
|
+
const workerStates = Array.from(sessionWorkerStates.values());
|
|
720
|
+
const processingCount = workerStates.filter((state) => String(state?.status || '').trim().toLowerCase() === 'processing').length;
|
|
721
|
+
const errorCount = workerStates.filter((state) => String(state?.uiState || state?.derivedUiState || '').trim().toLowerCase() === 'error').length;
|
|
722
|
+
const questionCount = workerStates.filter((state) => String(state?.uiState || state?.derivedUiState || '').trim().toLowerCase() === 'question').length;
|
|
723
|
+
const dotState = resolveRelayDotState({
|
|
724
|
+
relayOnline,
|
|
725
|
+
cliOnline,
|
|
726
|
+
cloudflaredTunnel: cloudflaredTunnelState,
|
|
727
|
+
processingCount,
|
|
728
|
+
errorCount,
|
|
729
|
+
questionCount,
|
|
730
|
+
});
|
|
731
|
+
if (dot) {
|
|
732
|
+
dot.className = dotState.className;
|
|
733
|
+
dot.title = dotState.title;
|
|
734
|
+
}
|
|
735
|
+
// The dot lives in the sidebar, which is an off-canvas drawer on mobile. The
|
|
736
|
+
// burger toggle stays on screen, so it carries the same tone on its bars.
|
|
737
|
+
if (burger) burger.dataset.relayTone = dotState.tone;
|
|
738
|
+
if (text) text.textContent = cliOnline ? 'CLI online' : 'CLI offline';
|
|
739
|
+
if (changeCwdBtn) {
|
|
740
|
+
changeCwdBtn.disabled = false;
|
|
741
|
+
changeCwdBtn.title = 'Change the next launch CWD';
|
|
742
|
+
}
|
|
743
|
+
if (banner) {
|
|
744
|
+
if (cliOnline) banner.classList.remove('visible');
|
|
745
|
+
else banner.classList.add('visible');
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export function setCliOnline(value) {
|
|
750
|
+
const nextOnline = !!value;
|
|
751
|
+
const changed = cliOnline !== nextOnline;
|
|
752
|
+
cliOnline = nextOnline;
|
|
753
|
+
if (changed) recordCliLifecycleEvent(nextOnline);
|
|
754
|
+
updateCliStatus();
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
export function setRelayOnline(value) {
|
|
758
|
+
const nextOnline = !!value;
|
|
759
|
+
const changed = relayOnline !== nextOnline;
|
|
760
|
+
relayOnline = nextOnline;
|
|
761
|
+
if (changed) recordRelayLifecycleEvent(nextOnline);
|
|
762
|
+
updateCliStatus();
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
export function setCloudflaredTunnelState(value) {
|
|
766
|
+
cloudflaredTunnelState = value && typeof value === 'object' ? value : null;
|
|
767
|
+
updateCliStatus();
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export function setImageEditTarget(value) {
|
|
771
|
+
imageEditTarget = value && typeof value === 'object'
|
|
772
|
+
? {
|
|
773
|
+
messageId: String(value.messageId || '').trim(),
|
|
774
|
+
imageId: String(value.imageId || '').trim(),
|
|
775
|
+
nodeId: String(value.nodeId || '').trim() || null,
|
|
776
|
+
name: String(value.name || 'generated image').trim() || 'generated image',
|
|
777
|
+
}
|
|
778
|
+
: null;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function normalizeWorkerSessionId(value) {
|
|
782
|
+
const text = String(value || '').trim();
|
|
783
|
+
return text || '';
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function normalizeWorkerStatus(value) {
|
|
787
|
+
const text = String(value || '').trim().toLowerCase();
|
|
788
|
+
return text || 'unknown';
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const CANONICAL_UI_STATES = new Set(['offline', 'ready', 'question', 'error']);
|
|
792
|
+
const UI_STATE_ALIAS_MAP = new Map([
|
|
793
|
+
['offline', 'offline'],
|
|
794
|
+
['inactive', 'offline'],
|
|
795
|
+
['unknown', 'offline'],
|
|
796
|
+
['new', 'offline'],
|
|
797
|
+
['disconnected', 'offline'],
|
|
798
|
+
['stopped', 'offline'],
|
|
799
|
+
['idle', 'offline'],
|
|
800
|
+
['white', 'offline'],
|
|
801
|
+
['healthy', 'ready'],
|
|
802
|
+
['ready', 'ready'],
|
|
803
|
+
['online', 'ready'],
|
|
804
|
+
['active', 'ready'],
|
|
805
|
+
['busy', 'ready'],
|
|
806
|
+
['processing', 'ready'],
|
|
807
|
+
['starting', 'ready'],
|
|
808
|
+
['green', 'ready'],
|
|
809
|
+
['question', 'question'],
|
|
810
|
+
['question-pending', 'question'],
|
|
811
|
+
['question_pending', 'question'],
|
|
812
|
+
['awaiting-input', 'question'],
|
|
813
|
+
['awaiting_input', 'question'],
|
|
814
|
+
['needs-input', 'question'],
|
|
815
|
+
['needs_input', 'question'],
|
|
816
|
+
['input-required', 'question'],
|
|
817
|
+
['input_required', 'question'],
|
|
818
|
+
['waiting-for-input', 'question'],
|
|
819
|
+
['waiting_for_input', 'question'],
|
|
820
|
+
['red', 'question'],
|
|
821
|
+
['error', 'error'],
|
|
822
|
+
['degraded', 'error'],
|
|
823
|
+
['unsafe', 'error'],
|
|
824
|
+
['failed', 'error'],
|
|
825
|
+
['yellow', 'error'],
|
|
826
|
+
]);
|
|
827
|
+
|
|
828
|
+
function normalizeUiState(value) {
|
|
829
|
+
const text = String(value || '').trim().toLowerCase();
|
|
830
|
+
if (!text) return '';
|
|
831
|
+
if (CANONICAL_UI_STATES.has(text)) return text;
|
|
832
|
+
return UI_STATE_ALIAS_MAP.get(text) || '';
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function normalizeUiStateFromStatus(value) {
|
|
836
|
+
return normalizeUiState(normalizeWorkerStatus(value));
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
export function resolveConversationUiState({ conversation = null, workerState = null, hasPendingQuestion = false } = {}) {
|
|
840
|
+
const backendUiState = normalizeUiState(
|
|
841
|
+
workerState?.uiState
|
|
842
|
+
|| workerState?.ui_state
|
|
843
|
+
|| conversation?.runtimeSessionUiState
|
|
844
|
+
|| conversation?.runtime_session_ui_state,
|
|
845
|
+
);
|
|
846
|
+
if (backendUiState) return backendUiState;
|
|
847
|
+
if (hasPendingQuestion) return 'question';
|
|
848
|
+
|
|
849
|
+
const fallbackUiState = normalizeUiState(
|
|
850
|
+
workerState?.derivedUiState
|
|
851
|
+
|| workerState?.fallbackUiState
|
|
852
|
+
|| normalizeUiStateFromStatus(workerState?.status)
|
|
853
|
+
|| normalizeUiStateFromStatus(conversation?.runtimeSessionStatus)
|
|
854
|
+
|| normalizeUiStateFromStatus(conversation?.runtime_session_status),
|
|
855
|
+
);
|
|
856
|
+
return fallbackUiState || 'offline';
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
function normalizeWorkerStateEntry(worker) {
|
|
860
|
+
const sdkSessionId = normalizeWorkerSessionId(worker?.sdkSessionId);
|
|
861
|
+
if (!sdkSessionId) return null;
|
|
862
|
+
const explicitUiState = normalizeUiState(worker?.uiState || worker?.ui_state);
|
|
863
|
+
const derivedUiState = normalizeUiStateFromStatus(worker?.status);
|
|
864
|
+
return {
|
|
865
|
+
sdkSessionId,
|
|
866
|
+
status: normalizeWorkerStatus(worker?.status),
|
|
867
|
+
uiState: explicitUiState || null,
|
|
868
|
+
derivedUiState: derivedUiState || null,
|
|
869
|
+
workerId: String(worker?.workerId || '').trim() || null,
|
|
870
|
+
pid: Number.isInteger(Number(worker?.pid)) ? Number(worker.pid) : null,
|
|
871
|
+
updatedAt: String(worker?.updatedAt || '').trim() || null,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
function buildSessionWorkerStateHash(map) {
|
|
876
|
+
const parts = [];
|
|
877
|
+
for (const [sid, state] of map.entries()) {
|
|
878
|
+
parts.push(`${sid}:${state.status}:${state.uiState || ''}:${state.derivedUiState || ''}:${state.workerId || ''}:${state.pid || ''}:${state.updatedAt || ''}`);
|
|
879
|
+
}
|
|
880
|
+
return parts.sort().join('|');
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
let sessionWorkerStateHash = '';
|
|
884
|
+
|
|
885
|
+
export function setSessionWorkerStatesFromStatusPayload(payload) {
|
|
886
|
+
const workers = Array.isArray(payload?.workers) ? payload.workers : [];
|
|
887
|
+
const next = new Map();
|
|
888
|
+
for (const worker of workers) {
|
|
889
|
+
const normalized = normalizeWorkerStateEntry(worker);
|
|
890
|
+
if (!normalized) continue;
|
|
891
|
+
next.set(normalized.sdkSessionId, normalized);
|
|
892
|
+
}
|
|
893
|
+
const nextHash = buildSessionWorkerStateHash(next);
|
|
894
|
+
if (nextHash === sessionWorkerStateHash) return false;
|
|
895
|
+
sessionWorkerStateHash = nextHash;
|
|
896
|
+
sessionWorkerStates = next;
|
|
897
|
+
return true;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
export function getSessionWorkerState(sdkSessionId) {
|
|
901
|
+
const sid = normalizeWorkerSessionId(sdkSessionId);
|
|
902
|
+
if (!sid) return null;
|
|
903
|
+
return sessionWorkerStates.get(sid) || null;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
function isPortraitViewport() {
|
|
907
|
+
return !!window.matchMedia('(orientation: portrait)').matches;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function isOverlaySidebarViewport() {
|
|
911
|
+
return window.matchMedia('(max-width: 680px)').matches;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function syncMobileSidebarTopOffset() {
|
|
915
|
+
const header = document.getElementById('chat-header');
|
|
916
|
+
const top = isOverlaySidebarViewport() && header
|
|
917
|
+
? Math.max(0, Math.ceil(header.getBoundingClientRect().bottom))
|
|
918
|
+
: 0;
|
|
919
|
+
document.documentElement.style.setProperty('--mobile-sidebar-top', `${top}px`);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
function parseCssPx(value) {
|
|
923
|
+
const numeric = Number.parseFloat(String(value || ''));
|
|
924
|
+
return Number.isFinite(numeric) ? numeric : 0;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function isVisibleElement(el) {
|
|
928
|
+
if (!el) return false;
|
|
929
|
+
const style = window.getComputedStyle(el);
|
|
930
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function measureSidebarHeaderMinWidthPx() {
|
|
934
|
+
const header = document.querySelector('#sidebar .sidebar-header');
|
|
935
|
+
if (!header) return 0;
|
|
936
|
+
const headerStyle = window.getComputedStyle(header);
|
|
937
|
+
const sidePadding = parseCssPx(headerStyle.paddingLeft) + parseCssPx(headerStyle.paddingRight);
|
|
938
|
+
const headerGap = parseCssPx(headerStyle.columnGap || headerStyle.gap);
|
|
939
|
+
|
|
940
|
+
let neededWidth = sidePadding;
|
|
941
|
+
let sectionCount = 0;
|
|
942
|
+
|
|
943
|
+
const statusDot = document.getElementById('cli-dot');
|
|
944
|
+
if (isVisibleElement(statusDot)) {
|
|
945
|
+
neededWidth += Math.ceil(statusDot.getBoundingClientRect().width || 0);
|
|
946
|
+
sectionCount += 1;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
const newConversationButton = document.getElementById('new-conv-btn');
|
|
950
|
+
if (isVisibleElement(newConversationButton)) {
|
|
951
|
+
const buttonStyle = window.getComputedStyle(newConversationButton);
|
|
952
|
+
neededWidth += Math.ceil(parseCssPx(buttonStyle.minWidth));
|
|
953
|
+
sectionCount += 1;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const statusButton = document.getElementById('status-btn');
|
|
957
|
+
if (isVisibleElement(statusButton)) {
|
|
958
|
+
const buttonStyle = window.getComputedStyle(statusButton);
|
|
959
|
+
neededWidth += Math.ceil(parseCssPx(buttonStyle.minWidth));
|
|
960
|
+
sectionCount += 1;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const actions = document.getElementById('sidebar-actions');
|
|
964
|
+
if (isVisibleElement(actions)) {
|
|
965
|
+
const actionsStyle = window.getComputedStyle(actions);
|
|
966
|
+
const actionGap = parseCssPx(actionsStyle.columnGap || actionsStyle.gap);
|
|
967
|
+
const visibleButtons = Array.from(actions.children).filter(child => isVisibleElement(child));
|
|
968
|
+
let actionsWidth = 0;
|
|
969
|
+
for (const button of visibleButtons) {
|
|
970
|
+
actionsWidth += Math.ceil(button.getBoundingClientRect().width || 0);
|
|
971
|
+
}
|
|
972
|
+
if (visibleButtons.length > 1) actionsWidth += actionGap * (visibleButtons.length - 1);
|
|
973
|
+
neededWidth += actionsWidth;
|
|
974
|
+
sectionCount += 1;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
if (sectionCount > 1) {
|
|
978
|
+
neededWidth += headerGap * (sectionCount - 1);
|
|
979
|
+
}
|
|
980
|
+
return Math.ceil(neededWidth + 4);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function sidebarWidthPercentMin() {
|
|
984
|
+
const viewportWidth = Math.max(window.innerWidth || 0, document.documentElement.clientWidth || 0, 1);
|
|
985
|
+
const headerMinPx = measureSidebarHeaderMinWidthPx();
|
|
986
|
+
const headerMinPercent = (headerMinPx / viewportWidth) * 100;
|
|
987
|
+
return Math.min(SIDEBAR_WIDTH_PERCENT_MAX, Math.max(SIDEBAR_WIDTH_PERCENT_MIN, headerMinPercent));
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function clampSidebarWidthPercent(value) {
|
|
991
|
+
const numeric = Number(value);
|
|
992
|
+
if (!Number.isFinite(numeric)) return SIDEBAR_WIDTH_PERCENT_FALLBACK;
|
|
993
|
+
const minPercent = sidebarWidthPercentMin();
|
|
994
|
+
if (numeric <= minPercent) return minPercent;
|
|
995
|
+
if (numeric >= SIDEBAR_WIDTH_PERCENT_MAX) return SIDEBAR_WIDTH_PERCENT_MAX;
|
|
996
|
+
return Math.round(numeric * 100) / 100;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function currentSidebarWidthStorageKey() {
|
|
1000
|
+
return isPortraitViewport()
|
|
1001
|
+
? SIDEBAR_WIDTH_PORTRAIT_STORAGE_KEY
|
|
1002
|
+
: SIDEBAR_WIDTH_LANDSCAPE_STORAGE_KEY;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function oppositeSidebarWidthStorageKey() {
|
|
1006
|
+
return isPortraitViewport()
|
|
1007
|
+
? SIDEBAR_WIDTH_LANDSCAPE_STORAGE_KEY
|
|
1008
|
+
: SIDEBAR_WIDTH_PORTRAIT_STORAGE_KEY;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
function readSidebarWidthPercentForCurrentOrientation() {
|
|
1012
|
+
const primary = Number(localStorage.getItem(currentSidebarWidthStorageKey()));
|
|
1013
|
+
if (Number.isFinite(primary)) return clampSidebarWidthPercent(primary);
|
|
1014
|
+
const fallback = Number(localStorage.getItem(oppositeSidebarWidthStorageKey()));
|
|
1015
|
+
if (Number.isFinite(fallback)) return clampSidebarWidthPercent(fallback);
|
|
1016
|
+
return SIDEBAR_WIDTH_PERCENT_FALLBACK;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function persistSidebarWidthPercentForCurrentOrientation(percent) {
|
|
1020
|
+
localStorage.setItem(currentSidebarWidthStorageKey(), String(clampSidebarWidthPercent(percent)));
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function applySidebarWidthPercent(percent, { persist = true } = {}) {
|
|
1024
|
+
sidebarWidthPercent = clampSidebarWidthPercent(percent);
|
|
1025
|
+
const viewportWidth = Math.max(window.innerWidth || 0, document.documentElement.clientWidth || 0, 1);
|
|
1026
|
+
const widthPx = Math.round((viewportWidth * sidebarWidthPercent) / 100);
|
|
1027
|
+
document.documentElement.style.setProperty('--sidebar-width', `${Math.max(1, widthPx)}px`);
|
|
1028
|
+
if (persist) {
|
|
1029
|
+
persistSidebarWidthPercentForCurrentOrientation(sidebarWidthPercent);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function readDesktopSidebarCollapsed() {
|
|
1034
|
+
return localStorage.getItem(SIDEBAR_COLLAPSED_DESKTOP_STORAGE_KEY) === '1';
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function setDesktopSidebarCollapsed(collapsed) {
|
|
1038
|
+
const next = !!collapsed;
|
|
1039
|
+
document.body.classList.toggle('sidebar-collapsed', next);
|
|
1040
|
+
localStorage.setItem(SIDEBAR_COLLAPSED_DESKTOP_STORAGE_KEY, next ? '1' : '0');
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
export function isSidebarOpen() {
|
|
1044
|
+
const sidebar = document.getElementById('sidebar');
|
|
1045
|
+
if (isOverlaySidebarViewport()) {
|
|
1046
|
+
return !!sidebar?.classList.contains('open');
|
|
1047
|
+
}
|
|
1048
|
+
return !document.body.classList.contains('sidebar-collapsed');
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
export function openSidebar() {
|
|
1052
|
+
if (isOverlaySidebarViewport()) {
|
|
1053
|
+
document.body.classList.remove('sidebar-collapsed');
|
|
1054
|
+
document.getElementById('sidebar')?.classList.add('open');
|
|
1055
|
+
document.getElementById('sidebar-overlay')?.classList.add('visible');
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
setDesktopSidebarCollapsed(false);
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
export function closeSidebar() {
|
|
1062
|
+
document.getElementById('sidebar')?.classList.remove('open');
|
|
1063
|
+
document.getElementById('sidebar-overlay')?.classList.remove('visible');
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
export function toggleSidebar(forceOpen = null) {
|
|
1067
|
+
if (isOverlaySidebarViewport()) {
|
|
1068
|
+
const shouldOpen = typeof forceOpen === 'boolean' ? forceOpen : !isSidebarOpen();
|
|
1069
|
+
if (shouldOpen) openSidebar();
|
|
1070
|
+
else closeSidebar();
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const nextOpen = typeof forceOpen === 'boolean' ? forceOpen : !isSidebarOpen();
|
|
1074
|
+
setDesktopSidebarCollapsed(!nextOpen);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
export function syncSidebarLayoutState() {
|
|
1078
|
+
applySidebarWidthPercent(readSidebarWidthPercentForCurrentOrientation(), { persist: false });
|
|
1079
|
+
if (isOverlaySidebarViewport()) {
|
|
1080
|
+
document.body.classList.remove('sidebar-collapsed');
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
document.getElementById('sidebar')?.classList.remove('open');
|
|
1084
|
+
document.getElementById('sidebar-overlay')?.classList.remove('visible');
|
|
1085
|
+
setDesktopSidebarCollapsed(readDesktopSidebarCollapsed());
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function beginSidebarResize(pointerDownEvent) {
|
|
1089
|
+
if (pointerDownEvent.button !== 0) return;
|
|
1090
|
+
if (isOverlaySidebarViewport() || !isSidebarOpen()) return;
|
|
1091
|
+
const startX = Number(pointerDownEvent.clientX || 0);
|
|
1092
|
+
const startPercent = sidebarWidthPercent;
|
|
1093
|
+
document.body.classList.add('sidebar-resizing');
|
|
1094
|
+
pointerDownEvent.preventDefault();
|
|
1095
|
+
|
|
1096
|
+
const onPointerMove = (moveEvent) => {
|
|
1097
|
+
const viewportWidth = Math.max(window.innerWidth || 0, document.documentElement.clientWidth || 0, 1);
|
|
1098
|
+
const deltaX = Number(moveEvent.clientX || 0) - startX;
|
|
1099
|
+
const deltaPercent = (deltaX / viewportWidth) * 100;
|
|
1100
|
+
applySidebarWidthPercent(startPercent + deltaPercent, { persist: true });
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
const onPointerUp = () => {
|
|
1104
|
+
document.body.classList.remove('sidebar-resizing');
|
|
1105
|
+
window.removeEventListener('pointermove', onPointerMove);
|
|
1106
|
+
window.removeEventListener('pointerup', onPointerUp);
|
|
1107
|
+
window.removeEventListener('pointercancel', onPointerUp);
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
window.addEventListener('pointermove', onPointerMove);
|
|
1111
|
+
window.addEventListener('pointerup', onPointerUp);
|
|
1112
|
+
window.addEventListener('pointercancel', onPointerUp);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
export function initSidebarLayout() {
|
|
1116
|
+
syncMobileSidebarTopOffset();
|
|
1117
|
+
syncSidebarLayoutState();
|
|
1118
|
+
if (window.__sidebarLayoutBound) return;
|
|
1119
|
+
window.__sidebarLayoutBound = true;
|
|
1120
|
+
|
|
1121
|
+
const handle = document.getElementById('sidebar-resizer');
|
|
1122
|
+
if (handle) {
|
|
1123
|
+
handle.addEventListener('pointerdown', beginSidebarResize);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
const resync = () => syncSidebarLayoutState();
|
|
1127
|
+
window.addEventListener('resize', resync, { passive: true });
|
|
1128
|
+
window.addEventListener('orientationchange', resync, { passive: true });
|
|
1129
|
+
const chatHeader = document.getElementById('chat-header');
|
|
1130
|
+
if (chatHeader && typeof ResizeObserver === 'function') {
|
|
1131
|
+
const observer = new ResizeObserver(syncMobileSidebarTopOffset);
|
|
1132
|
+
observer.observe(chatHeader);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
export function setCompactInFlight(value) {
|
|
1137
|
+
compactInFlight = !!value;
|
|
1138
|
+
updateCompactButton();
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
export function isCompactInFlight() {
|
|
1142
|
+
return compactInFlight;
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
export function setHistoryRefreshInFlight(value) {
|
|
1146
|
+
historyRefreshInFlight = !!value;
|
|
1147
|
+
const refreshBtn = document.getElementById('chat-menu-refresh-history');
|
|
1148
|
+
if (refreshBtn) {
|
|
1149
|
+
if (refreshBtn.hidden || refreshBtn.getAttribute('aria-hidden') === 'true') {
|
|
1150
|
+
refreshBtn.disabled = true;
|
|
1151
|
+
refreshBtn.tabIndex = -1;
|
|
1152
|
+
refreshBtn.setAttribute('aria-disabled', 'true');
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
refreshBtn.disabled = historyRefreshInFlight;
|
|
1156
|
+
refreshBtn.textContent = historyRefreshInFlight ? '🔄️ Refreshing…' : '🔄️ Refresh history';
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
export function isHistoryRefreshInFlight() {
|
|
1161
|
+
return historyRefreshInFlight;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
export function setModelBanner(message) {
|
|
1165
|
+
const el = document.getElementById('model-banner');
|
|
1166
|
+
if (!el) return;
|
|
1167
|
+
const text = String(message || '').trim();
|
|
1168
|
+
if (!text) {
|
|
1169
|
+
el.textContent = '';
|
|
1170
|
+
el.classList.remove('visible');
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
el.textContent = text;
|
|
1174
|
+
el.classList.add('visible');
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
export function showTransientRelayNotice(message, ms = 4000) {
|
|
1178
|
+
const text = String(message || '').trim();
|
|
1179
|
+
if (!text) return;
|
|
1180
|
+
setModelBanner(text);
|
|
1181
|
+
setTimeout(() => {
|
|
1182
|
+
const el = document.getElementById('model-banner');
|
|
1183
|
+
if (!el) return;
|
|
1184
|
+
if (String(el.textContent || '').trim() === text) {
|
|
1185
|
+
setModelBanner('');
|
|
1186
|
+
}
|
|
1187
|
+
}, Math.max(1500, Number(ms) || 4000));
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
export function syncViewportMetrics() {
|
|
1191
|
+
const layoutHeight = window.innerHeight || document.documentElement.clientHeight || 0;
|
|
1192
|
+
const layoutWidth = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1193
|
+
const vv = window.visualViewport;
|
|
1194
|
+
let appHeight = layoutHeight;
|
|
1195
|
+
let viewportTopOffset = 0;
|
|
1196
|
+
|
|
1197
|
+
if (vv) {
|
|
1198
|
+
const visibleHeight = Math.max(0, vv.height || 0);
|
|
1199
|
+
const offsetTop = Math.max(0, vv.offsetTop || 0);
|
|
1200
|
+
viewportTopOffset = offsetTop;
|
|
1201
|
+
const baselineCandidate = Math.max(layoutHeight, visibleHeight + offsetTop);
|
|
1202
|
+
if (baselineCandidate > viewportBaseHeight) viewportBaseHeight = baselineCandidate;
|
|
1203
|
+
appHeight = baselineCandidate;
|
|
1204
|
+
} else if (layoutHeight > viewportBaseHeight) {
|
|
1205
|
+
viewportBaseHeight = layoutHeight;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
document.documentElement.style.setProperty('--viewport-top-offset', `${Math.max(0, Math.round(viewportTopOffset))}px`);
|
|
1209
|
+
document.documentElement.style.setProperty('--app-height', `${Math.max(0, Math.round(appHeight))}px`);
|
|
1210
|
+
const messagesEl = document.getElementById('messages');
|
|
1211
|
+
const messagesHeight = messagesEl ? Math.max(0, messagesEl.clientHeight || 0) : appHeight;
|
|
1212
|
+
const messagesWidth = messagesEl ? Math.max(0, messagesEl.clientWidth || 0) : layoutWidth;
|
|
1213
|
+
document.documentElement.style.setProperty('--messages-height', `${Math.max(0, Math.round(messagesHeight))}px`);
|
|
1214
|
+
document.documentElement.style.setProperty('--messages-width', `${Math.max(0, Math.round(messagesWidth))}px`);
|
|
1215
|
+
syncMobileSidebarTopOffset();
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
export let summaryModalState = {
|
|
1219
|
+
kind: '',
|
|
1220
|
+
refresh: null,
|
|
1221
|
+
loading: false,
|
|
1222
|
+
};
|
|
1223
|
+
|
|
1224
|
+
export function setSummaryModalLoading(loading) {
|
|
1225
|
+
summaryModalState.loading = !!loading;
|
|
1226
|
+
const refreshBtn = document.getElementById('summary-modal-refresh');
|
|
1227
|
+
if (refreshBtn) {
|
|
1228
|
+
refreshBtn.hidden = !summaryModalState.refresh;
|
|
1229
|
+
refreshBtn.disabled = summaryModalState.loading || !summaryModalState.refresh;
|
|
1230
|
+
refreshBtn.textContent = summaryModalState.loading ? 'Loading…' : 'Refresh';
|
|
1231
|
+
}
|
|
1232
|
+
// Freeze/unfreeze all action buttons inside the modal body while a request is in flight.
|
|
1233
|
+
// data-keep-disabled marks buttons that were rendered disabled for their own reason
|
|
1234
|
+
// (e.g. no launchable session) and must not be re-enabled when loading clears.
|
|
1235
|
+
const bodyEl = document.getElementById('summary-modal-body');
|
|
1236
|
+
if (bodyEl) {
|
|
1237
|
+
for (const btn of bodyEl.querySelectorAll('button')) {
|
|
1238
|
+
if (btn.dataset.keepDisabled === '1') {
|
|
1239
|
+
btn.disabled = true;
|
|
1240
|
+
continue;
|
|
1241
|
+
}
|
|
1242
|
+
btn.disabled = summaryModalState.loading;
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
export function renderSummaryModalContent({ title, subtitle = '', bodyHtml = '', refresh = null, kind = '' }) {
|
|
1248
|
+
summaryModalState.kind = String(kind || '').trim();
|
|
1249
|
+
summaryModalState.refresh = typeof refresh === 'function' ? refresh : null;
|
|
1250
|
+
|
|
1251
|
+
const titleEl = document.getElementById('summary-modal-title');
|
|
1252
|
+
const subtitleEl = document.getElementById('summary-modal-subtitle');
|
|
1253
|
+
const bodyEl = document.getElementById('summary-modal-body');
|
|
1254
|
+
const staleModelSaveBtn = document.getElementById('summary-modal-save-models');
|
|
1255
|
+
if (staleModelSaveBtn) staleModelSaveBtn.remove();
|
|
1256
|
+
if (titleEl) titleEl.textContent = String(title || 'Details').trim() || 'Details';
|
|
1257
|
+
if (subtitleEl) subtitleEl.textContent = String(subtitle || '').trim();
|
|
1258
|
+
if (bodyEl) bodyEl.innerHTML = bodyHtml || '';
|
|
1259
|
+
setSummaryModalLoading(false);
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
export function openSummaryModal({ title, subtitle = '', bodyHtml = '', refresh = null, kind = '' }) {
|
|
1263
|
+
renderSummaryModalContent({ title, subtitle, bodyHtml, refresh, kind });
|
|
1264
|
+
const modal = document.getElementById('summary-modal');
|
|
1265
|
+
modal?.classList.add('visible');
|
|
1266
|
+
modal?.setAttribute('aria-hidden', 'false');
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
export function closeSummaryModal() {
|
|
1270
|
+
summaryModalState.kind = '';
|
|
1271
|
+
summaryModalState.refresh = null;
|
|
1272
|
+
summaryModalState.loading = false;
|
|
1273
|
+
const modal = document.getElementById('summary-modal');
|
|
1274
|
+
modal?.classList.remove('visible');
|
|
1275
|
+
modal?.setAttribute('aria-hidden', 'true');
|
|
1276
|
+
const titleEl = document.getElementById('summary-modal-title');
|
|
1277
|
+
const subtitleEl = document.getElementById('summary-modal-subtitle');
|
|
1278
|
+
const bodyEl = document.getElementById('summary-modal-body');
|
|
1279
|
+
if (titleEl) titleEl.textContent = 'Details';
|
|
1280
|
+
if (subtitleEl) subtitleEl.textContent = '';
|
|
1281
|
+
if (bodyEl) bodyEl.innerHTML = '';
|
|
1282
|
+
setSummaryModalLoading(false);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
export async function refreshSummaryModal() {
|
|
1286
|
+
if (!summaryModalState.refresh || summaryModalState.loading) return;
|
|
1287
|
+
setSummaryModalLoading(true);
|
|
1288
|
+
try {
|
|
1289
|
+
await summaryModalState.refresh();
|
|
1290
|
+
} finally {
|
|
1291
|
+
setSummaryModalLoading(false);
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
export function normalizeSubagentRunId(value) {
|
|
1296
|
+
return String(value || '').trim() || null;
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
export function normalizeSubagentStatus(value) {
|
|
1300
|
+
const text = String(value || '').trim().toLowerCase();
|
|
1301
|
+
if (['pending', 'processing', 'running', 'completed', 'failed', 'cancelled'].includes(text)) return text;
|
|
1302
|
+
return 'running';
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
export function markSubagentCancelInFlight(subagentRunId) {
|
|
1306
|
+
const id = normalizeSubagentRunId(subagentRunId);
|
|
1307
|
+
if (!id) return false;
|
|
1308
|
+
subagentCancelInFlight.add(id);
|
|
1309
|
+
return true;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
export function clearSubagentCancelInFlight(subagentRunId) {
|
|
1313
|
+
const id = normalizeSubagentRunId(subagentRunId);
|
|
1314
|
+
if (!id) return false;
|
|
1315
|
+
return subagentCancelInFlight.delete(id);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
export function isSubagentCancelInFlight(subagentRunId) {
|
|
1319
|
+
const id = normalizeSubagentRunId(subagentRunId);
|
|
1320
|
+
return id ? subagentCancelInFlight.has(id) : false;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
export function upsertSubagentRun(payload) {
|
|
1324
|
+
const subagentRunId = normalizeSubagentRunId(payload?.subagentRunId);
|
|
1325
|
+
if (!subagentRunId) return null;
|
|
1326
|
+
|
|
1327
|
+
const existing = subagentRuns.get(subagentRunId);
|
|
1328
|
+
const now = new Date().toISOString();
|
|
1329
|
+
const entry = {
|
|
1330
|
+
subagentRunId,
|
|
1331
|
+
messageId: String(payload?.messageId || existing?.messageId || '').trim() || null,
|
|
1332
|
+
conversationId: String(payload?.conversationId || existing?.conversationId || '').trim() || null,
|
|
1333
|
+
parentSubagentId: normalizeSubagentRunId(payload?.parentSubagentId) || existing?.parentSubagentId || null,
|
|
1334
|
+
displayName: String(payload?.displayName || existing?.displayName || '').trim() || null,
|
|
1335
|
+
status: normalizeSubagentStatus(payload?.status) || existing?.status || 'running',
|
|
1336
|
+
startedAt: existing?.startedAt || payload?.timestamp || now,
|
|
1337
|
+
updatedAt: payload?.timestamp || now,
|
|
1338
|
+
activities: existing?.activities || [],
|
|
1339
|
+
thoughts: existing?.thoughts || [],
|
|
1340
|
+
// Cumulative streamed text for this run, replayed when its bubble is
|
|
1341
|
+
// (re)built — bubbles can appear after the first stream frame arrives.
|
|
1342
|
+
streamText: existing?.streamText || '',
|
|
1343
|
+
};
|
|
1344
|
+
|
|
1345
|
+
subagentRuns.set(subagentRunId, entry);
|
|
1346
|
+
return entry;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
export function getSubagentRun(subagentRunId) {
|
|
1350
|
+
const id = normalizeSubagentRunId(subagentRunId);
|
|
1351
|
+
return id ? subagentRuns.get(id) || null : null;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
export function getSubagentRunsByMessage(messageId) {
|
|
1355
|
+
const id = String(messageId || '').trim();
|
|
1356
|
+
if (!id) return [];
|
|
1357
|
+
const results = [];
|
|
1358
|
+
for (const entry of subagentRuns.values()) {
|
|
1359
|
+
if (entry.messageId === id) results.push(entry);
|
|
1360
|
+
}
|
|
1361
|
+
return results.sort((a, b) => {
|
|
1362
|
+
const aTime = parseTimestampMs(a.startedAt);
|
|
1363
|
+
const bTime = parseTimestampMs(b.startedAt);
|
|
1364
|
+
return aTime - bTime;
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
export function getChildSubagentRuns(parentSubagentId) {
|
|
1369
|
+
const id = normalizeSubagentRunId(parentSubagentId);
|
|
1370
|
+
if (!id) return [];
|
|
1371
|
+
const results = [];
|
|
1372
|
+
for (const entry of subagentRuns.values()) {
|
|
1373
|
+
if (entry.parentSubagentId === id) results.push(entry);
|
|
1374
|
+
}
|
|
1375
|
+
return results.sort((a, b) => {
|
|
1376
|
+
const aTime = parseTimestampMs(a.startedAt);
|
|
1377
|
+
const bTime = parseTimestampMs(b.startedAt);
|
|
1378
|
+
return aTime - bTime;
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
export function getRootSubagentRunsByMessage(messageId) {
|
|
1383
|
+
const id = String(messageId || '').trim();
|
|
1384
|
+
if (!id) return [];
|
|
1385
|
+
const results = [];
|
|
1386
|
+
for (const entry of subagentRuns.values()) {
|
|
1387
|
+
if (entry.messageId === id && !entry.parentSubagentId) {
|
|
1388
|
+
results.push(entry);
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
return results.sort((a, b) => {
|
|
1392
|
+
const aTime = parseTimestampMs(a.startedAt);
|
|
1393
|
+
const bTime = parseTimestampMs(b.startedAt);
|
|
1394
|
+
return aTime - bTime;
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
export function addSubagentActivity(subagentRunId, activityText) {
|
|
1399
|
+
const entry = getSubagentRun(subagentRunId);
|
|
1400
|
+
if (!entry) return false;
|
|
1401
|
+
const text = String(activityText || '').trim();
|
|
1402
|
+
if (!text) return false;
|
|
1403
|
+
entry.activities.push({ text, timestamp: new Date().toISOString() });
|
|
1404
|
+
return true;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
export function addSubagentThought(subagentRunId, thoughtPayload) {
|
|
1408
|
+
const entry = getSubagentRun(subagentRunId);
|
|
1409
|
+
if (!entry) return false;
|
|
1410
|
+
const text = String(thoughtPayload?.text || '').trim();
|
|
1411
|
+
if (!text) return false;
|
|
1412
|
+
entry.thoughts.push({
|
|
1413
|
+
reasoningId: String(thoughtPayload?.reasoningId || '').trim() || null,
|
|
1414
|
+
text,
|
|
1415
|
+
done: !!thoughtPayload?.done,
|
|
1416
|
+
timestamp: thoughtPayload?.timestamp || new Date().toISOString(),
|
|
1417
|
+
});
|
|
1418
|
+
return true;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
export function setSubagentStreamText(subagentRunId, streamText) {
|
|
1422
|
+
const entry = getSubagentRun(subagentRunId);
|
|
1423
|
+
if (!entry) return false;
|
|
1424
|
+
entry.streamText = String(streamText || '');
|
|
1425
|
+
return true;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
export function clearSubagentRunsForMessage(messageId) {
|
|
1429
|
+
const id = String(messageId || '').trim();
|
|
1430
|
+
if (!id) return 0;
|
|
1431
|
+
let cleared = 0;
|
|
1432
|
+
for (const [runId, entry] of subagentRuns.entries()) {
|
|
1433
|
+
if (entry.messageId === id) {
|
|
1434
|
+
subagentRuns.delete(runId);
|
|
1435
|
+
subagentCancelInFlight.delete(runId);
|
|
1436
|
+
cleared += 1;
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
return cleared;
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
export function clearSubagentRunsForConversation(conversationId) {
|
|
1443
|
+
const id = String(conversationId || '').trim();
|
|
1444
|
+
if (!id) return 0;
|
|
1445
|
+
let cleared = 0;
|
|
1446
|
+
for (const [runId, entry] of subagentRuns.entries()) {
|
|
1447
|
+
if (entry.conversationId === id) {
|
|
1448
|
+
subagentRuns.delete(runId);
|
|
1449
|
+
subagentCancelInFlight.delete(runId);
|
|
1450
|
+
cleared += 1;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return cleared;
|
|
1454
|
+
}
|