@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,703 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BASE,
|
|
3
|
+
TOKEN,
|
|
4
|
+
CLIENT_ID,
|
|
5
|
+
DEVICE_ID,
|
|
6
|
+
currentConvId,
|
|
7
|
+
conversations,
|
|
8
|
+
seenMessageIds,
|
|
9
|
+
pendingUserMessageIds,
|
|
10
|
+
relayActivities,
|
|
11
|
+
relayThoughts,
|
|
12
|
+
relayQuestions,
|
|
13
|
+
relayBoards,
|
|
14
|
+
relayQuestionDrafts,
|
|
15
|
+
repoBrowserState,
|
|
16
|
+
setRelayOnline,
|
|
17
|
+
setCliOnline,
|
|
18
|
+
setCloudflaredTunnelState,
|
|
19
|
+
setCurrentConv,
|
|
20
|
+
updateWorkspaceRootHints,
|
|
21
|
+
updateCompactButton,
|
|
22
|
+
updateSessionPill,
|
|
23
|
+
clearPendingUserMessage,
|
|
24
|
+
hasPendingUserMessageDuplicate,
|
|
25
|
+
isMessagesAtBottom,
|
|
26
|
+
upsertSubagentRun,
|
|
27
|
+
addSubagentActivity,
|
|
28
|
+
addSubagentThought,
|
|
29
|
+
clearSubagentCancelInFlight,
|
|
30
|
+
setConversationWatcherCount,
|
|
31
|
+
} from './store.js';
|
|
32
|
+
import { scheduleContextUsageRefresh } from './api-client.js';
|
|
33
|
+
import { publishStatusEvent, recordStatusEvent } from './status-store.mjs';
|
|
34
|
+
import { renderConvList, refreshConversations, openConversation } from './journal-view.js';
|
|
35
|
+
import {
|
|
36
|
+
upsertRelayQuestion,
|
|
37
|
+
loadRelayQuestions,
|
|
38
|
+
updatePendingQuestionBanner,
|
|
39
|
+
} from './ask-user-view.js';
|
|
40
|
+
import { upsertRelayBoard, loadRelayBoards, renderRelayBoards } from './relay-board-view.js';
|
|
41
|
+
import { setConversationBackgroundTasks } from './background-tasks-view.mjs';
|
|
42
|
+
import { setPreviews } from './preview-cards.mjs';
|
|
43
|
+
import { applyClaudeAuthState } from './claude-auth-ui.js';
|
|
44
|
+
import { applyGrokAuthState } from './grok-auth-ui.js';
|
|
45
|
+
import { applyCliInstallState } from './cli-install-ui.js';
|
|
46
|
+
import {
|
|
47
|
+
showThinking,
|
|
48
|
+
removeThinking,
|
|
49
|
+
collapseThinkingThoughts,
|
|
50
|
+
renderThinkingActivities,
|
|
51
|
+
renderThinkingThoughts,
|
|
52
|
+
renderRestoredSubagentBubbles,
|
|
53
|
+
appendThinkingActivity,
|
|
54
|
+
appendThinkingThought,
|
|
55
|
+
applyRelayStreamEvent,
|
|
56
|
+
clearRelayStreamStateForMessage,
|
|
57
|
+
applyConversationTurnStatus,
|
|
58
|
+
renderMessages,
|
|
59
|
+
appendMessage,
|
|
60
|
+
applyIncomingConversationDraftUpdate,
|
|
61
|
+
getRenderedConversationMessageFingerprints,
|
|
62
|
+
clearBubbleCancelState,
|
|
63
|
+
removeUserBubbleCancelButton,
|
|
64
|
+
updateSubagentBubbleFromStatus,
|
|
65
|
+
markSubagentStopUnsupported,
|
|
66
|
+
} from './conversation-view.js';
|
|
67
|
+
import { loadRepoBrowserTree, refreshRepoBrowserIfWorkspaceOpen } from './attachments-view.js';
|
|
68
|
+
import { clearMessageSearchRuntimeState } from './message-search-view.js';
|
|
69
|
+
import { stripRelayPromptContext } from './relay-prompt-sanitizer.mjs';
|
|
70
|
+
import { isLikelyLiveDuplicateMessage } from './live-message-dedupe.mjs';
|
|
71
|
+
import { mergeRelayThoughts } from './relay-thoughts.mjs';
|
|
72
|
+
|
|
73
|
+
const FALLBACK_MODE = 'agent';
|
|
74
|
+
|
|
75
|
+
// How long a liveness probe waits for its ack before declaring the transport a
|
|
76
|
+
// zombie. Comfortably above one mobile round-trip, far below the ~45s the
|
|
77
|
+
// engine's own ping timeout would need — which is the timer an Android freeze
|
|
78
|
+
// may have discarded, making the probe necessary in the first place.
|
|
79
|
+
const SOCKET_LIVENESS_TIMEOUT_MS = 5000;
|
|
80
|
+
|
|
81
|
+
/** @type {import('socket.io-client').Socket | null} */
|
|
82
|
+
let socket = null;
|
|
83
|
+
let socketActivityEnabled = true;
|
|
84
|
+
let livenessProbeInFlight = false;
|
|
85
|
+
let lastSocketErrorSignature = '';
|
|
86
|
+
let lastSocketErrorAt = 0;
|
|
87
|
+
|
|
88
|
+
/** @type {SocketHandlerDeps | null} */
|
|
89
|
+
let deps = null;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @typedef {Object} SocketHandlerDeps
|
|
93
|
+
* @property {() => (void | Promise<void>)} refreshCurrentView
|
|
94
|
+
* @property {() => (void | Promise<void>)} refreshSessionWorkerStatus
|
|
95
|
+
* @property {(force?: boolean) => (void | Promise<void>)} refreshModelCatalog
|
|
96
|
+
* @property {(payload?: object) => void} updateModelCatalogState
|
|
97
|
+
* @property {() => (void | Promise<void>)} reconcileOpenModelVariantModal
|
|
98
|
+
* @property {(payload?: object) => void} applyConversationWorkspaceRootUpdate
|
|
99
|
+
* @property {(conversationId: string, title: string, updatedAt?: string | number | null) => void} applyConversationTitleUpdate
|
|
100
|
+
* @property {() => void} syncChatTitleControls
|
|
101
|
+
* @property {(conversationId: string, payload?: object) => void} applyConversationPreferencesForConversation
|
|
102
|
+
* @property {(payload?: object) => void} applyOpenAISettingsState
|
|
103
|
+
* @property {(payload?: object) => void} applyClaudeSettingsState
|
|
104
|
+
* @property {(payload?: object) => void} applyGrokSettingsState
|
|
105
|
+
* @property {(payload?: object) => void} applyCursorSettingsState
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Register bootstrap-local callbacks required by socket event handlers.
|
|
110
|
+
* @param {SocketHandlerDeps} nextDeps
|
|
111
|
+
*/
|
|
112
|
+
export function initSocketHandlers(nextDeps) {
|
|
113
|
+
deps = nextDeps;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function getSocket() {
|
|
117
|
+
return socket;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Report this device's foreground state to the server, which uses it to decide
|
|
122
|
+
* whether push notifications should be suppressed. Safe to call in any state:
|
|
123
|
+
* a disconnected or absent socket makes it a no-op (the connect handler
|
|
124
|
+
* re-asserts visibility as soon as the socket is back).
|
|
125
|
+
*/
|
|
126
|
+
export function emitDeviceVisibility(visible) {
|
|
127
|
+
if (!socket?.connected) return;
|
|
128
|
+
socket.emit('device_visibility', { deviceId: DEVICE_ID, visible: visible === true });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Close the transport rather than the namespace socket when backgrounding.
|
|
133
|
+
*
|
|
134
|
+
* socket.disconnect() makes the server report "client namespace disconnect", which
|
|
135
|
+
* is absent from socket.io's RECOVERABLE_DISCONNECT_REASONS, so the session is
|
|
136
|
+
* discarded and connectionStateRecovery replays nothing when the phone returns.
|
|
137
|
+
* Closing the engine surfaces as "transport close", which is recoverable. Manager
|
|
138
|
+
* reconnection is suspended first so the close does not immediately trigger the
|
|
139
|
+
* backoff loop we are trying to avoid while hidden.
|
|
140
|
+
*/
|
|
141
|
+
function suspendSocketForBackground() {
|
|
142
|
+
// The "now hidden" heartbeat has to leave before the transport closes or it
|
|
143
|
+
// never arrives and the device looks active until its socket drops.
|
|
144
|
+
emitDeviceVisibility(false);
|
|
145
|
+
const manager = socket?.io;
|
|
146
|
+
manager?.reconnection(false);
|
|
147
|
+
if (manager?.engine) {
|
|
148
|
+
manager.engine.close();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (socket?.connected || socket?.active) socket.disconnect();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function resumeSocketFromBackground() {
|
|
155
|
+
socket?.io?.reconnection(true);
|
|
156
|
+
if (socket && !socket.connected) socket.connect();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function setSocketActivityEnabled(value) {
|
|
160
|
+
socketActivityEnabled = !!value;
|
|
161
|
+
if (!socket) return;
|
|
162
|
+
if (!socketActivityEnabled) {
|
|
163
|
+
suspendSocketForBackground();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
resumeSocketFromBackground();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Bring the relay socket back up, whatever state the manager is in. Safe to call on a
|
|
171
|
+
* timer: connect() skips the manager while it is mid-backoff, so this cannot compete
|
|
172
|
+
* with socket.io's own retry schedule.
|
|
173
|
+
*
|
|
174
|
+
* `socket.active` only distinguishes the two cases for the caller's benefit. It stays
|
|
175
|
+
* true while the manager works through its backoff, and goes false once it gives up
|
|
176
|
+
* for good — which is what happens after an explicit disconnect() or a rejected
|
|
177
|
+
* handshake, since both destroy the socket instead of scheduling a retry. That is the
|
|
178
|
+
* case worth reporting, because nothing else would have reconnected.
|
|
179
|
+
* @returns {'connected'|'retrying'|'forced'|'disabled'}
|
|
180
|
+
*/
|
|
181
|
+
export function ensureSocketConnected() {
|
|
182
|
+
if (!socketActivityEnabled || !socket) return 'disabled';
|
|
183
|
+
if (socket.connected) return 'connected';
|
|
184
|
+
const wasRetrying = socket.active;
|
|
185
|
+
// Reconnection is suspended while backgrounded, so re-arm it before asking the
|
|
186
|
+
// socket to connect. connect() is a no-op while the manager is mid-backoff.
|
|
187
|
+
socket.io?.reconnection(true);
|
|
188
|
+
socket.connect();
|
|
189
|
+
return wasRetrying ? 'retrying' : 'forced';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Last-resort recovery for manager states connect() cannot escape: a
|
|
194
|
+
* `_reconnecting` flag whose backoff timer was dropped by an Android freeze, or
|
|
195
|
+
* a connect attempt whose timers died with it. disconnect() forcibly clears the
|
|
196
|
+
* manager state machine (skipReconnect/_reconnecting) and destroys whatever
|
|
197
|
+
* engine is left; connect() then opens a clean handshake, and the socket's
|
|
198
|
+
* retained recovery offset still lets the server replay missed events when the
|
|
199
|
+
* outage stayed inside the recovery window.
|
|
200
|
+
*/
|
|
201
|
+
export function hardResetSocket() {
|
|
202
|
+
if (!socket) return;
|
|
203
|
+
try {
|
|
204
|
+
socket.disconnect();
|
|
205
|
+
} catch {}
|
|
206
|
+
if (!socketActivityEnabled) return;
|
|
207
|
+
socket.io?.reconnection(true);
|
|
208
|
+
socket.connect();
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Distrust a socket that claims to be connected right after the app returns to
|
|
213
|
+
* the foreground. A page frozen mid-connection can resume with `connected`
|
|
214
|
+
* still true on a transport the server dropped hours ago, and if the engine's
|
|
215
|
+
* ping-timeout timer was discarded with the freeze nothing ever notices — the
|
|
216
|
+
* watchdog sees "connected" and stands down. An acked ping settles it: no ack
|
|
217
|
+
* within the timeout means zombie, and closing the engine hands the manager a
|
|
218
|
+
* real close event to reconnect from.
|
|
219
|
+
*/
|
|
220
|
+
export function verifySocketLiveness() {
|
|
221
|
+
if (!socketActivityEnabled || !socket?.connected || livenessProbeInFlight) return;
|
|
222
|
+
livenessProbeInFlight = true;
|
|
223
|
+
socket.timeout(SOCKET_LIVENESS_TIMEOUT_MS).emit('client_ping', (err) => {
|
|
224
|
+
livenessProbeInFlight = false;
|
|
225
|
+
if (!err) return;
|
|
226
|
+
// The engine may already have noticed and reconnected (or been suspended)
|
|
227
|
+
// while the probe was outstanding; only kill a socket still playing alive.
|
|
228
|
+
if (!socket?.connected) return;
|
|
229
|
+
recordStatusEvent('relay-zombie-socket', { timeoutMs: SOCKET_LIVENESS_TIMEOUT_MS });
|
|
230
|
+
socket.io?.engine?.close();
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function requireDeps() {
|
|
235
|
+
if (!deps) {
|
|
236
|
+
throw new Error('socket-handlers: call initSocketHandlers() before connectSocket()');
|
|
237
|
+
}
|
|
238
|
+
return deps;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export async function connectSocket(overrideDeps) {
|
|
242
|
+
if (overrideDeps) {
|
|
243
|
+
deps = overrideDeps;
|
|
244
|
+
}
|
|
245
|
+
const {
|
|
246
|
+
refreshCurrentView,
|
|
247
|
+
refreshSessionWorkerStatus,
|
|
248
|
+
refreshModelCatalog,
|
|
249
|
+
updateModelCatalogState,
|
|
250
|
+
reconcileOpenModelVariantModal = async () => {},
|
|
251
|
+
applyConversationWorkspaceRootUpdate,
|
|
252
|
+
applyConversationTitleUpdate,
|
|
253
|
+
syncChatTitleControls,
|
|
254
|
+
applyConversationPreferencesForConversation,
|
|
255
|
+
} = requireDeps();
|
|
256
|
+
|
|
257
|
+
if (socket) {
|
|
258
|
+
if (socketActivityEnabled && !socket.connected) socket.connect();
|
|
259
|
+
return socket;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
socket = io({
|
|
263
|
+
path: `${BASE}/socket.io/`,
|
|
264
|
+
auth: TOKEN ? { token: TOKEN, clientId: CLIENT_ID } : { clientId: CLIENT_ID },
|
|
265
|
+
autoConnect: false,
|
|
266
|
+
reconnection: true,
|
|
267
|
+
reconnectionDelay: 1000,
|
|
268
|
+
reconnectionDelayMax: 10000,
|
|
269
|
+
randomizationFactor: 0.5,
|
|
270
|
+
timeout: 10000,
|
|
271
|
+
transports: ['websocket', 'polling'],
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
// The connect-time view resync is the only correction path for state whose
|
|
275
|
+
// change events this client missed while suspended (e.g. a background-task
|
|
276
|
+
// set the server has since emptied — an empty store never re-announces
|
|
277
|
+
// itself). A connect that races the relay's own restart loses a one-shot
|
|
278
|
+
// resync to a failed fetch and the stale state then survives indefinitely,
|
|
279
|
+
// so failures retry with backoff until one pass lands on a live server.
|
|
280
|
+
let connectResyncTimer = null;
|
|
281
|
+
let connectResyncGeneration = 0;
|
|
282
|
+
function resyncAfterConnect(attempt = 0) {
|
|
283
|
+
const generation = attempt === 0 ? ++connectResyncGeneration : connectResyncGeneration;
|
|
284
|
+
if (connectResyncTimer) {
|
|
285
|
+
clearTimeout(connectResyncTimer);
|
|
286
|
+
connectResyncTimer = null;
|
|
287
|
+
}
|
|
288
|
+
refreshCurrentView().catch(() => {
|
|
289
|
+
if (generation !== connectResyncGeneration || attempt >= 4 || !socket?.connected) return;
|
|
290
|
+
connectResyncTimer = setTimeout(() => {
|
|
291
|
+
connectResyncTimer = null;
|
|
292
|
+
resyncAfterConnect(attempt + 1);
|
|
293
|
+
}, 2000 * (attempt + 1));
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
socket.on('connect', () => {
|
|
298
|
+
lastSocketErrorSignature = '';
|
|
299
|
+
lastSocketErrorAt = 0;
|
|
300
|
+
console.log('Socket connected');
|
|
301
|
+
// Connect-time heartbeat. Also re-asserts visibility after a recovered
|
|
302
|
+
// session, whose restored socket.data the server deliberately resets.
|
|
303
|
+
emitDeviceVisibility(document.visibilityState === 'visible');
|
|
304
|
+
clearMessageSearchRuntimeState();
|
|
305
|
+
setRelayOnline(true);
|
|
306
|
+
setCliOnline(true);
|
|
307
|
+
renderConvList();
|
|
308
|
+
resyncAfterConnect();
|
|
309
|
+
refreshSessionWorkerStatus().catch(() => {});
|
|
310
|
+
refreshModelCatalog().catch(() => {});
|
|
311
|
+
});
|
|
312
|
+
socket.on('connect_error', (e) => {
|
|
313
|
+
setRelayOnline(false);
|
|
314
|
+
const message = String(e?.message || 'unknown').trim() || 'unknown';
|
|
315
|
+
const signature = `socket-error:${message}`;
|
|
316
|
+
const now = Date.now();
|
|
317
|
+
if (signature !== lastSocketErrorSignature || (now - lastSocketErrorAt) > 8000) {
|
|
318
|
+
lastSocketErrorSignature = signature;
|
|
319
|
+
lastSocketErrorAt = now;
|
|
320
|
+
console.error('Socket error:', message);
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
socket.on('disconnect', () => {
|
|
324
|
+
setRelayOnline(false);
|
|
325
|
+
});
|
|
326
|
+
socket.on('cli_status', ({ online }) => {
|
|
327
|
+
setCliOnline(online);
|
|
328
|
+
renderConvList();
|
|
329
|
+
if (online) refreshCurrentView().catch(() => {});
|
|
330
|
+
refreshSessionWorkerStatus().catch(() => {});
|
|
331
|
+
if (online) refreshModelCatalog().catch(() => {});
|
|
332
|
+
});
|
|
333
|
+
// Keeps the relay dot's Cloudflare Tunnel colour live between status polls.
|
|
334
|
+
socket.on('cloudflared_tunnel_status', (payload) => {
|
|
335
|
+
setCloudflaredTunnelState(payload || null);
|
|
336
|
+
});
|
|
337
|
+
socket.on('models_updated', (payload) => {
|
|
338
|
+
updateModelCatalogState(payload || {});
|
|
339
|
+
void reconcileOpenModelVariantModal();
|
|
340
|
+
});
|
|
341
|
+
socket.on('openai_settings_updated', (payload) => {
|
|
342
|
+
deps?.applyOpenAISettingsState?.(payload || {});
|
|
343
|
+
if (Number(payload?.reconciliation?.updatedUnstartedConversations || 0) > 0) {
|
|
344
|
+
void Promise.resolve()
|
|
345
|
+
.then(() => deps?.refreshCurrentView?.())
|
|
346
|
+
.catch(() => {});
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
// No view refresh here: the engine switch rebinds nothing — it only changes
|
|
350
|
+
// which worker the NEXT Copilot conversation spawns.
|
|
351
|
+
socket.on('copilot_settings_updated', (payload) => {
|
|
352
|
+
deps?.applyCopilotSettingsState?.(payload || {});
|
|
353
|
+
});
|
|
354
|
+
socket.on('claude_settings_updated', (payload) => {
|
|
355
|
+
deps?.applyClaudeSettingsState?.(payload || {});
|
|
356
|
+
if (Number(payload?.reconciliation?.updatedUnstartedConversations || 0) > 0) {
|
|
357
|
+
void Promise.resolve()
|
|
358
|
+
.then(() => deps?.refreshCurrentView?.())
|
|
359
|
+
.catch(() => {});
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
// Login/logout transitions on the relay host. Broadcast to every client so a
|
|
363
|
+
// login started on the desktop can be finished on a phone (and vice versa).
|
|
364
|
+
socket.on('claude_auth_state', (payload) => {
|
|
365
|
+
applyClaudeAuthState(payload || null);
|
|
366
|
+
});
|
|
367
|
+
// Same contract for Grok, one state shorter (no code to paste back). Success
|
|
368
|
+
// arrives twice — once off the cached status, once when the confirming read
|
|
369
|
+
// lands — so the renderer is idempotent rather than transition-driven.
|
|
370
|
+
socket.on('grok_auth_state', (payload) => {
|
|
371
|
+
applyGrokAuthState(payload || null);
|
|
372
|
+
});
|
|
373
|
+
// CLI install transitions AND log chunks. The payload always carries the whole
|
|
374
|
+
// retained buffer plus a monotonic logSeq, so a client that connects
|
|
375
|
+
// mid-install renders the full log instead of a suffix.
|
|
376
|
+
socket.on('cli_install_state', (payload) => {
|
|
377
|
+
applyCliInstallState(payload || null);
|
|
378
|
+
});
|
|
379
|
+
socket.on('grok_settings_updated', (payload) => {
|
|
380
|
+
deps?.applyGrokSettingsState?.(payload || {});
|
|
381
|
+
if (Number(payload?.reconciliation?.updatedUnstartedConversations || 0) > 0) {
|
|
382
|
+
void Promise.resolve()
|
|
383
|
+
.then(() => deps?.refreshCurrentView?.())
|
|
384
|
+
.catch(() => {});
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
socket.on('cursor_settings_updated', (payload) => {
|
|
388
|
+
deps?.applyCursorSettingsState?.(payload || {});
|
|
389
|
+
if (Number(payload?.reconciliation?.updatedUnstartedConversations || 0) > 0) {
|
|
390
|
+
void Promise.resolve()
|
|
391
|
+
.then(() => deps?.refreshCurrentView?.())
|
|
392
|
+
.catch(() => {});
|
|
393
|
+
}
|
|
394
|
+
});
|
|
395
|
+
socket.on('shared_access', (event) => {
|
|
396
|
+
publishStatusEvent(event);
|
|
397
|
+
});
|
|
398
|
+
socket.on('workspace_root_changed', (payload) => {
|
|
399
|
+
updateWorkspaceRootHints(payload || {});
|
|
400
|
+
if (repoBrowserState.activeRoot !== 'workspace') return;
|
|
401
|
+
repoBrowserState.currentPath = '';
|
|
402
|
+
if (repoBrowserState.open) {
|
|
403
|
+
void loadRepoBrowserTree();
|
|
404
|
+
}
|
|
405
|
+
});
|
|
406
|
+
socket.on('conversation_workspace_root_updated', (payload) => {
|
|
407
|
+
updateWorkspaceRootHints(payload || {});
|
|
408
|
+
applyConversationWorkspaceRootUpdate(payload || {});
|
|
409
|
+
});
|
|
410
|
+
socket.on('user_message', ({ conversationId, messageId, senderClientId, message }) => {
|
|
411
|
+
const normalizedMessage = {
|
|
412
|
+
...(message && typeof message === 'object' ? message : {}),
|
|
413
|
+
text: stripRelayPromptContext(message?.text, message?.mode),
|
|
414
|
+
};
|
|
415
|
+
if (senderClientId && senderClientId === CLIENT_ID) {
|
|
416
|
+
pendingUserMessageIds.delete(messageId);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (messageId && (pendingUserMessageIds.has(messageId) || seenMessageIds?.has(messageId))) {
|
|
420
|
+
pendingUserMessageIds.delete(messageId);
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
if (conversationId === currentConvId) {
|
|
424
|
+
const renderedMessages = getRenderedConversationMessageFingerprints(24);
|
|
425
|
+
const hasPendingTextMatch = hasPendingUserMessageDuplicate(conversationId, normalizedMessage.text);
|
|
426
|
+
if (isLikelyLiveDuplicateMessage({
|
|
427
|
+
incomingMessageId: messageId,
|
|
428
|
+
incomingMessage: normalizedMessage,
|
|
429
|
+
existingMessages: renderedMessages,
|
|
430
|
+
hasPendingTextMatch,
|
|
431
|
+
})) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
appendMessage(normalizedMessage, true, messageId);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
socket.on('assistant_message', ({ conversationId, message, messageId, sourceMessageId }) => {
|
|
438
|
+
const isCurrentConversation = conversationId === currentConvId;
|
|
439
|
+
const autoScroll = isCurrentConversation ? isMessagesAtBottom() : false;
|
|
440
|
+
collapseThinkingThoughts();
|
|
441
|
+
removeThinking();
|
|
442
|
+
if ((!message?.activities || !message.activities.length) && sourceMessageId) {
|
|
443
|
+
const cached = relayActivities.get(sourceMessageId) || [];
|
|
444
|
+
if (cached.length) message.activities = cached.slice(0, 48);
|
|
445
|
+
}
|
|
446
|
+
if (sourceMessageId) {
|
|
447
|
+
const persistedThoughts = Array.isArray(message?.thoughts) ? message.thoughts : [];
|
|
448
|
+
const cachedThoughts = relayThoughts.get(sourceMessageId);
|
|
449
|
+
const mergedThoughts = mergeRelayThoughts(persistedThoughts, cachedThoughts);
|
|
450
|
+
if (mergedThoughts.length) {
|
|
451
|
+
message.thoughts = mergedThoughts;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (messageId && seenMessageIds?.has(messageId)) return;
|
|
455
|
+
if (isCurrentConversation) {
|
|
456
|
+
appendMessage(message, autoScroll, messageId || null, false, sourceMessageId || null);
|
|
457
|
+
scheduleContextUsageRefresh(conversationId, 120);
|
|
458
|
+
}
|
|
459
|
+
if (sourceMessageId) relayActivities.delete(sourceMessageId);
|
|
460
|
+
if (sourceMessageId) relayThoughts.delete(sourceMessageId);
|
|
461
|
+
if (sourceMessageId) clearRelayStreamStateForMessage(sourceMessageId);
|
|
462
|
+
refreshSessionWorkerStatus().catch(() => {});
|
|
463
|
+
});
|
|
464
|
+
socket.on('relay_question', ({ question }) => upsertRelayQuestion(question));
|
|
465
|
+
socket.on('relay_question_updated', ({ question }) => upsertRelayQuestion(question));
|
|
466
|
+
socket.on('relay_question_changed', () => {
|
|
467
|
+
loadRelayQuestions(currentConvId);
|
|
468
|
+
});
|
|
469
|
+
socket.on('relay_board', ({ board }) => upsertRelayBoard(board));
|
|
470
|
+
socket.on('relay_board_updated', ({ board }) => upsertRelayBoard(board));
|
|
471
|
+
socket.on('relay_board_changed', () => {
|
|
472
|
+
loadRelayBoards();
|
|
473
|
+
});
|
|
474
|
+
socket.on('background_tasks', ({ conversationId, tasks }) => {
|
|
475
|
+
setConversationBackgroundTasks(conversationId, tasks);
|
|
476
|
+
});
|
|
477
|
+
// REPLACE semantics for the whole set: the preview registry is relay-owned
|
|
478
|
+
// and global, so every change ships the full list.
|
|
479
|
+
socket.on('previews', ({ previews }) => {
|
|
480
|
+
setPreviews(previews);
|
|
481
|
+
});
|
|
482
|
+
socket.on('relay_activity', ({ conversationId, messageId, text, subagentRunId, metadata }) => {
|
|
483
|
+
if (!messageId || !text) return;
|
|
484
|
+
const entry = {
|
|
485
|
+
text: String(text || '').trim(),
|
|
486
|
+
subagentRunId: subagentRunId ? String(subagentRunId).trim() : null,
|
|
487
|
+
metadata: (metadata && typeof metadata === 'object' && !Array.isArray(metadata)) ? metadata : null,
|
|
488
|
+
};
|
|
489
|
+
if (!entry.text) return;
|
|
490
|
+
const items = relayActivities.get(messageId) || [];
|
|
491
|
+
const last = items[items.length - 1];
|
|
492
|
+
const lastText = typeof last === 'string' ? last : String(last?.text || '');
|
|
493
|
+
const lastSubagentRunId = typeof last === 'object' && last ? (last.subagentRunId || null) : null;
|
|
494
|
+
if (lastText !== entry.text || lastSubagentRunId !== entry.subagentRunId) {
|
|
495
|
+
relayActivities.set(messageId, items.concat(entry).slice(-24));
|
|
496
|
+
}
|
|
497
|
+
if (entry.subagentRunId) {
|
|
498
|
+
upsertSubagentRun({ subagentRunId: entry.subagentRunId, messageId, conversationId });
|
|
499
|
+
addSubagentActivity(entry.subagentRunId, entry.text);
|
|
500
|
+
}
|
|
501
|
+
if (conversationId === currentConvId) {
|
|
502
|
+
const autoScroll = isMessagesAtBottom();
|
|
503
|
+
appendThinkingActivity(entry, entry.subagentRunId, autoScroll);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
socket.on('relay_stream', ({ conversationId, messageId, text, done, seq, subagentRunId }) => {
|
|
507
|
+
if (!messageId) return;
|
|
508
|
+
if (subagentRunId) {
|
|
509
|
+
upsertSubagentRun({ subagentRunId: String(subagentRunId).trim(), messageId, conversationId });
|
|
510
|
+
}
|
|
511
|
+
if (conversationId !== currentConvId) return;
|
|
512
|
+
const autoScroll = isMessagesAtBottom();
|
|
513
|
+
applyRelayStreamEvent({
|
|
514
|
+
messageId,
|
|
515
|
+
text: String(text || ''),
|
|
516
|
+
done: !!done,
|
|
517
|
+
seq,
|
|
518
|
+
subagentRunId: subagentRunId ? String(subagentRunId).trim() : null,
|
|
519
|
+
autoScroll,
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
socket.on('relay_thought', ({ conversationId, messageId, reasoningId, text, done, subagentRunId }) => {
|
|
523
|
+
if (!messageId) return;
|
|
524
|
+
const key = String(reasoningId || 'reasoning');
|
|
525
|
+
const thoughtMap = relayThoughts.get(messageId) || new Map();
|
|
526
|
+
thoughtMap.set(key, { reasoningId: key, text: String(text || ''), done: !!done, subagentRunId: subagentRunId || null });
|
|
527
|
+
relayThoughts.set(messageId, thoughtMap);
|
|
528
|
+
if (subagentRunId) {
|
|
529
|
+
upsertSubagentRun({ subagentRunId, messageId, conversationId });
|
|
530
|
+
addSubagentThought(subagentRunId, { reasoningId: key, text: String(text || ''), done: !!done });
|
|
531
|
+
}
|
|
532
|
+
if (conversationId === currentConvId) {
|
|
533
|
+
const autoScroll = isMessagesAtBottom();
|
|
534
|
+
appendThinkingThought(key, String(text || ''), !!done, subagentRunId, autoScroll);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
socket.on('subagent_status', ({ conversationId, messageId, subagentRunId, parentSubagentId, displayName, status, timestamp, stopUnsupported }) => {
|
|
538
|
+
if (!messageId || !subagentRunId) return;
|
|
539
|
+
upsertSubagentRun({
|
|
540
|
+
subagentRunId,
|
|
541
|
+
messageId,
|
|
542
|
+
conversationId,
|
|
543
|
+
parentSubagentId,
|
|
544
|
+
displayName,
|
|
545
|
+
status,
|
|
546
|
+
timestamp,
|
|
547
|
+
});
|
|
548
|
+
clearSubagentCancelInFlight(subagentRunId);
|
|
549
|
+
// The provider answered "not supported": pin the state so the button does
|
|
550
|
+
// not re-arm into an endless click-and-fail loop.
|
|
551
|
+
if (stopUnsupported) markSubagentStopUnsupported(subagentRunId);
|
|
552
|
+
if (conversationId === currentConvId) {
|
|
553
|
+
updateSubagentBubbleFromStatus(subagentRunId, status);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
socket.on('conversation_compacted', async ({ sourceConversationId, targetConversationId }) => {
|
|
557
|
+
if (!sourceConversationId || !targetConversationId) return;
|
|
558
|
+
await refreshConversations();
|
|
559
|
+
if (currentConvId === sourceConversationId) {
|
|
560
|
+
await openConversation(targetConversationId);
|
|
561
|
+
} else {
|
|
562
|
+
updateCompactButton();
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
socket.on('conversation_title_updated', ({ conversationId, title, updatedAt }) => {
|
|
566
|
+
applyConversationTitleUpdate(conversationId, title, updatedAt);
|
|
567
|
+
syncChatTitleControls();
|
|
568
|
+
});
|
|
569
|
+
socket.on('conversation_preferences_updated', ({ conversationId, preferredRelayMode, preferredModel, preferredReasoningEffort, autoCompactWindow, thinkingEnabled, thinkingDisplay, senderClientId }) => {
|
|
570
|
+
if (senderClientId && senderClientId === CLIENT_ID) return;
|
|
571
|
+
const id = String(conversationId || '').trim();
|
|
572
|
+
if (!id || !conversations[id]) return;
|
|
573
|
+
conversations[id] = {
|
|
574
|
+
...conversations[id],
|
|
575
|
+
preferredRelayMode: preferredRelayMode || conversations[id].preferredRelayMode || FALLBACK_MODE,
|
|
576
|
+
preferredModel: preferredModel || conversations[id].preferredModel || '',
|
|
577
|
+
preferredReasoningEffort: preferredReasoningEffort || conversations[id].preferredReasoningEffort || '',
|
|
578
|
+
// null is a real value here (Auto), so only an omitted field keeps the
|
|
579
|
+
// previously known one.
|
|
580
|
+
autoCompactWindow: autoCompactWindow === undefined
|
|
581
|
+
? (conversations[id].autoCompactWindow ?? null)
|
|
582
|
+
: (autoCompactWindow ?? null),
|
|
583
|
+
// Same semantics: null = Host default for enabled, and the display is
|
|
584
|
+
// an explicit mode string; only an omitted field keeps the known value.
|
|
585
|
+
thinkingEnabled: thinkingEnabled === undefined
|
|
586
|
+
? (conversations[id].thinkingEnabled ?? null)
|
|
587
|
+
: (thinkingEnabled ?? null),
|
|
588
|
+
thinkingDisplay: thinkingDisplay === undefined
|
|
589
|
+
? (conversations[id].thinkingDisplay ?? null)
|
|
590
|
+
: (thinkingDisplay ?? null),
|
|
591
|
+
};
|
|
592
|
+
if (String(currentConvId || '').trim() === id) {
|
|
593
|
+
applyConversationPreferencesForConversation(id, {
|
|
594
|
+
preferredRelayMode,
|
|
595
|
+
preferredModel,
|
|
596
|
+
preferredReasoningEffort,
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
socket.on('conversation_draft_updated', (payload = {}) => {
|
|
601
|
+
applyIncomingConversationDraftUpdate(payload || {});
|
|
602
|
+
});
|
|
603
|
+
socket.on('conversation_watchers', ({ conversationId, watcherCount }) => {
|
|
604
|
+
setConversationWatcherCount(conversationId, watcherCount);
|
|
605
|
+
deps?.syncChatTitleControls?.();
|
|
606
|
+
});
|
|
607
|
+
socket.on('conversation_session_bound', async ({ conversationId, sdkSessionId, runtimeSessionId }) => {
|
|
608
|
+
const id = String(conversationId || '').trim();
|
|
609
|
+
if (!id) return;
|
|
610
|
+
if (conversations[id]) {
|
|
611
|
+
conversations[id] = {
|
|
612
|
+
...conversations[id],
|
|
613
|
+
sdkSessionId: String(sdkSessionId || conversations[id].sdkSessionId || '').trim() || null,
|
|
614
|
+
runtimeSessionId: String(runtimeSessionId || conversations[id].runtimeSessionId || '').trim() || null,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
await refreshConversations();
|
|
618
|
+
if (currentConvId === id) {
|
|
619
|
+
await openConversation(id);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
socket.on('message_status', ({ messageId, conversationId, status }) => {
|
|
623
|
+
const normalizedStatus = String(status || '').trim().toLowerCase();
|
|
624
|
+
const clearsProcessingStatus = ['done', 'failed', 'dropped', 'pending', 'parked', 'cancelled'].includes(normalizedStatus);
|
|
625
|
+
// pending/parked fire mid-turn (queueing, park/re-queue); only genuinely
|
|
626
|
+
// terminal statuses may tear down the live bubble and reload the view.
|
|
627
|
+
const isTerminalStatus = ['done', 'failed', 'dropped', 'cancelled'].includes(normalizedStatus);
|
|
628
|
+
applyConversationTurnStatus({ conversationId, messageId, status });
|
|
629
|
+
if (conversationId && conversations[conversationId]) {
|
|
630
|
+
const conversation = conversations[conversationId];
|
|
631
|
+
if (normalizedStatus === 'processing') {
|
|
632
|
+
conversation.localTurnStatus = 'processing';
|
|
633
|
+
conversation.localTurnStatusUpdatedAt = Date.now();
|
|
634
|
+
conversation.localTurnMessageId = String(messageId || '').trim() || null;
|
|
635
|
+
} else if (clearsProcessingStatus) {
|
|
636
|
+
const trackedMessageId = String(conversation.localTurnMessageId || '').trim();
|
|
637
|
+
const incomingMessageId = String(messageId || '').trim();
|
|
638
|
+
if (!trackedMessageId || !incomingMessageId || trackedMessageId === incomingMessageId) {
|
|
639
|
+
delete conversation.localTurnStatus;
|
|
640
|
+
delete conversation.localTurnStatusUpdatedAt;
|
|
641
|
+
delete conversation.localTurnMessageId;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
if (conversationId === currentConvId && normalizedStatus === 'processing') {
|
|
646
|
+
const autoScroll = isMessagesAtBottom();
|
|
647
|
+
showThinking(messageId || null, autoScroll);
|
|
648
|
+
renderThinkingActivities();
|
|
649
|
+
renderThinkingThoughts();
|
|
650
|
+
if (messageId) renderRestoredSubagentBubbles(messageId);
|
|
651
|
+
if (messageId) removeUserBubbleCancelButton(messageId);
|
|
652
|
+
}
|
|
653
|
+
if (clearsProcessingStatus) {
|
|
654
|
+
clearPendingUserMessage(messageId);
|
|
655
|
+
if (messageId) clearRelayStreamStateForMessage(messageId);
|
|
656
|
+
if (messageId) clearBubbleCancelState(messageId);
|
|
657
|
+
if (messageId) removeUserBubbleCancelButton(messageId);
|
|
658
|
+
}
|
|
659
|
+
if (conversationId === currentConvId && isTerminalStatus) {
|
|
660
|
+
collapseThinkingThoughts();
|
|
661
|
+
removeThinking();
|
|
662
|
+
void refreshCurrentView().catch(() => {});
|
|
663
|
+
scheduleContextUsageRefresh(conversationId, 220);
|
|
664
|
+
refreshSessionWorkerStatus().catch(() => {});
|
|
665
|
+
// Files the agent created during the turn appear now, via the restoring
|
|
666
|
+
// path that keeps open folders and the current selection.
|
|
667
|
+
refreshRepoBrowserIfWorkspaceOpen();
|
|
668
|
+
}
|
|
669
|
+
renderConvList();
|
|
670
|
+
});
|
|
671
|
+
socket.on('conversation_deleted', ({ conversationId }) => {
|
|
672
|
+
delete conversations[conversationId];
|
|
673
|
+
for (const [id, question] of relayQuestions.entries()) {
|
|
674
|
+
if (question?.conversationId === conversationId) relayQuestions.delete(id);
|
|
675
|
+
}
|
|
676
|
+
for (const [id, board] of relayBoards.entries()) {
|
|
677
|
+
if (board?.conversationId === conversationId) relayBoards.delete(id);
|
|
678
|
+
}
|
|
679
|
+
for (const id of relayQuestionDrafts.keys()) {
|
|
680
|
+
const q = relayQuestionDrafts.get(id);
|
|
681
|
+
if (!q || q.conversationId === conversationId) relayQuestionDrafts.delete(id);
|
|
682
|
+
}
|
|
683
|
+
updatePendingQuestionBanner();
|
|
684
|
+
renderRelayBoards();
|
|
685
|
+
renderConvList();
|
|
686
|
+
if (currentConvId === conversationId) {
|
|
687
|
+
setCurrentConv(null);
|
|
688
|
+
renderMessages([]);
|
|
689
|
+
document.getElementById('chat-title').textContent = 'Select or start a conversation';
|
|
690
|
+
syncChatTitleControls();
|
|
691
|
+
updateSessionPill(null, null);
|
|
692
|
+
updateCompactButton();
|
|
693
|
+
scheduleContextUsageRefresh(null);
|
|
694
|
+
} else {
|
|
695
|
+
updateCompactButton();
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
if (socketActivityEnabled) {
|
|
700
|
+
socket.connect();
|
|
701
|
+
}
|
|
702
|
+
return socket;
|
|
703
|
+
}
|