@oar-sh/oar 0.9.9 → 0.9.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +561 -1
- package/README.md +162 -909
- package/bin/bat/oar.bat +46 -46
- package/package.json +14 -14
- package/server/README.md +369 -13
- package/server/claude-worker/claude-remote-relay-tool.mjs +10 -0
- package/server/claude-worker/claude-session-process.mjs +121 -9
- package/server/claude-worker/claude-session-worker.mjs +9 -1
- package/server/claude-worker/claude-turn-publisher.mjs +3 -0
- package/server/claude-worker/sdk-message-normalizer.mjs +31 -4
- package/server/claude-worker/subagent-timeline-recorder.mjs +572 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +79 -12
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +6 -0
- package/server/cursor-worker/cursor-sdk-adapter.mjs +56 -2
- package/server/db-schema.mjs +95 -2
- package/server/grok-worker/acp-client.mjs +2 -1
- package/server/grok-worker/acp-host-services.mjs +4 -3
- package/server/migrations/0009-subagent-timeline.mjs +135 -0
- package/server/migrations/0010-subagent-timeline-pruning.mjs +89 -0
- package/server/migrations/0011-relay-event-subscriptions.mjs +125 -0
- package/server/migrations/0012-cross-relay-event-subscriptions.mjs +116 -0
- package/server/migrations/0013-previews.mjs +130 -0
- package/server/migrations/0014-conversation-groups.mjs +89 -0
- package/server/migrations/0015-conversation-group-position.mjs +76 -0
- package/server/migrations/0016-share-access-log.mjs +110 -0
- package/server/public/app/about-ui.js +73 -0
- package/server/public/app/about-view.mjs +234 -0
- package/server/public/app/action-confirmations.js +31 -14
- package/server/public/app/agent-exchange-fold-view.mjs +187 -0
- package/server/public/app/api-client.js +312 -16
- package/server/public/app/archive-confirm.mjs +28 -0
- package/server/public/app/ask-user-view.js +17 -12
- package/server/public/app/attachments-view.js +516 -6
- package/server/public/app/background-tasks-view.mjs +18 -26
- package/server/public/app/bootstrap.js +336 -59
- package/server/public/app/chat-actions-menu-state.mjs +126 -0
- package/server/public/app/claude-cloud-conversation-ui.js +10 -22
- package/server/public/app/composer-control-state.mjs +16 -0
- package/server/public/app/composer-keyboard.mjs +54 -0
- package/server/public/app/context-menu.mjs +90 -6
- package/server/public/app/conversation-group-drag.mjs +191 -0
- package/server/public/app/conversation-group-order.mjs +265 -0
- package/server/public/app/conversation-group-state.mjs +100 -0
- package/server/public/app/conversation-list-drag.mjs +449 -0
- package/server/public/app/conversation-turn-state.mjs +81 -0
- package/server/public/app/conversation-view.js +436 -117
- package/server/public/app/cwd-picker.js +1 -1
- package/server/public/app/explorer-edit-state.mjs +68 -0
- package/server/public/app/hidden-messages-view.mjs +312 -0
- package/server/public/app/host-suspend-ui.js +13 -4
- package/server/public/app/journal-view.js +930 -45
- package/server/public/app/no-conversation-state.mjs +58 -0
- package/server/public/app/preview-cards.mjs +117 -31
- package/server/public/app/preview-command.mjs +10 -1
- package/server/public/app/provider-readiness.mjs +120 -0
- package/server/public/app/relay-board-view.js +19 -3
- package/server/public/app/relay-event-subscriptions-ui.js +184 -0
- package/server/public/app/relay-event-view.mjs +234 -0
- package/server/public/app/relay-restart-error.mjs +16 -0
- package/server/public/app/settings-modal.js +93 -9
- package/server/public/app/settings-tabs.js +1 -1
- package/server/public/app/share-format.mjs +49 -0
- package/server/public/app/share-preview-marks.mjs +167 -0
- package/server/public/app/share-state.js +109 -0
- package/server/public/app/shared-conversations-settings.js +480 -0
- package/server/public/app/socket-handlers.js +137 -40
- package/server/public/app/store.js +115 -32
- package/server/public/app/subagent-row.mjs +138 -0
- package/server/public/app/subagent-timeline-view.mjs +186 -0
- package/server/public/app/subagent-viewer-watch.mjs +50 -0
- package/server/public/app/subagent-viewer.mjs +558 -0
- package/server/public/app/task-row-format.mjs +29 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +464 -0
- package/server/public/app/transcript-jump-bottom.mjs +210 -0
- package/server/public/app/transcript-reload-freshness.mjs +25 -0
- package/server/public/app/transcript-viewport-anchor.mjs +140 -0
- package/server/public/app/update-ui.js +1 -1
- package/server/public/app/viewer-history.mjs +5 -3
- package/server/public/index.html +774 -12
- package/server/relay-tools.md +2 -1
- package/server/relay.mjs +2 -1
- package/server/repositories/conversation-group-repository.mjs +444 -0
- package/server/repositories/message-repository.mjs +30 -3
- package/server/repositories/question-repository.mjs +9 -3
- package/server/repositories/remote-relay-repository.mjs +6 -2
- package/server/repositories/session-repository.mjs +12 -37
- package/server/repositories/share-repository.mjs +322 -0
- package/server/repositories/share-visibility-statements.mjs +10 -0
- package/server/repositories/subagent-timeline-statements.mjs +170 -0
- package/server/routes/about-routes.mjs +15 -0
- package/server/routes/ask-user-routes.mjs +3 -0
- package/server/routes/cache-routes.mjs +4 -0
- package/server/routes/conversation-group-routes.mjs +171 -0
- package/server/routes/messages-routes.mjs +698 -114
- package/server/routes/preview-routes.mjs +9 -1
- package/server/routes/relay-event-routes.mjs +157 -0
- package/server/routes/remote-relay-routes.mjs +15 -0
- package/server/routes/sessions-routes.mjs +843 -193
- package/server/server-runtime.mjs +559 -160
- package/server/services/about-service.mjs +461 -0
- package/server/services/background-task-store.mjs +128 -0
- package/server/services/background-work-view.mjs +228 -0
- package/server/services/cache-rebuild-service.mjs +30 -0
- package/server/services/delete-archive-service.mjs +4 -1
- package/server/services/detached-worker-watch-service.mjs +150 -0
- package/server/services/explorer-entry-name.mjs +26 -0
- package/server/services/explorer-mutations.mjs +122 -0
- package/server/services/hidden-message-list.mjs +21 -0
- package/server/services/host-suspend-command.mjs +2 -1
- package/server/services/message-pin-list.mjs +1 -1
- package/server/services/preview-health-probe.mjs +8 -1
- package/server/services/preview-proxy-server.mjs +49 -27
- package/server/services/preview-registry-service.mjs +676 -26
- package/server/services/preview-static-handler.mjs +40 -7
- package/server/services/preview-store.mjs +274 -0
- package/server/services/push-dispatch-service.mjs +18 -0
- package/server/services/rate-limit-service.mjs +12 -1
- package/server/services/relay-auth.mjs +183 -0
- package/server/services/relay-cli-launcher-service.mjs +3 -1
- package/server/services/relay-event-link-service.mjs +289 -0
- package/server/services/relay-event-service.mjs +1952 -0
- package/server/services/relay-shutdown-gate.mjs +31 -0
- package/server/services/remote-relay-caller-context.mjs +52 -7
- package/server/services/remote-relay-dispatcher.mjs +551 -7
- package/server/services/remote-relay-registry-service.mjs +11 -1
- package/server/services/request-boundary.mjs +183 -0
- package/server/services/session-activity-tracker.mjs +695 -0
- package/server/services/session-history-refresh-service.mjs +7 -0
- package/server/services/session-worker-launch-service.mjs +190 -6
- package/server/services/session-worker-process-service.mjs +10 -6
- package/server/services/share-access-log-service.mjs +123 -0
- package/server/services/share-path-redaction.mjs +424 -0
- package/server/services/stale-attempt-log-limiter.mjs +96 -0
- package/server/services/stale-recovery-clock-guard.mjs +57 -0
- package/server/services/stale-subagent-run-sweep.mjs +109 -0
- package/server/services/status-event-service.mjs +7 -3
- package/server/services/subagent-record-retention.mjs +164 -0
- package/server/services/subagent-timeline-service.mjs +328 -0
- package/server/services/subagent-viewer-rooms.mjs +119 -0
- package/server/services/tunnel-worker-path-guard.mjs +24 -41
- package/server/services/upgrade-socket-guard.mjs +16 -0
- package/server/services/windows-autostart-service.mjs +6 -1
- package/server/services/windows-boot-autostart-service.mjs +207 -23
- package/shared/preview-tool-core.mjs +15 -5
- package/shared/relay-events.mjs +371 -0
- package/shared/remote-relay-contract.mjs +160 -6
- package/shared/share-path-privacy.mjs +30 -0
- package/shared/subagent-record-retention.mjs +31 -0
- package/shared/subagent-timeline.mjs +79 -0
- package/shared/windows-system-tools.mjs +106 -0
- package/shared/worker-runtime/background-task-digest.mjs +30 -0
- package/shared/worker-runtime/heartbeat.mjs +24 -0
- package/shared/worker-runtime/kept-posts.mjs +83 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +47 -15
|
@@ -16,14 +16,12 @@ import {
|
|
|
16
16
|
setRelayOnline,
|
|
17
17
|
setCliOnline,
|
|
18
18
|
setCloudflaredTunnelState,
|
|
19
|
-
setCurrentConv,
|
|
20
19
|
updateWorkspaceRootHints,
|
|
21
20
|
updateCompactButton,
|
|
22
|
-
updateSessionPill,
|
|
23
21
|
clearPendingUserMessage,
|
|
24
22
|
hasPendingUserMessageDuplicate,
|
|
25
|
-
isMessagesAtBottom,
|
|
26
23
|
upsertSubagentRun,
|
|
24
|
+
getSubagentRun,
|
|
27
25
|
addSubagentActivity,
|
|
28
26
|
addSubagentThought,
|
|
29
27
|
clearSubagentCancelInFlight,
|
|
@@ -36,7 +34,9 @@ import {
|
|
|
36
34
|
import { scheduleContextUsageRefresh } from './api-client.js';
|
|
37
35
|
import { capLiveActivityEntries } from './activity-replay-state.mjs';
|
|
38
36
|
import { publishStatusEvent, recordStatusEvent } from './status-store.mjs';
|
|
39
|
-
import { renderConvList, refreshConversations, openConversation } from './journal-view.js';
|
|
37
|
+
import { clearOpenConversation, renderConvList, refreshConversations, scheduleLoadedConversationsRefresh, applyConversationGroupEvent, openConversation } from './journal-view.js';
|
|
38
|
+
import { applyBackgroundTasksToConversation, applyTurnStatusToConversation } from './conversation-turn-state.mjs';
|
|
39
|
+
import { applyRelayEventSubscriptions, loadRelayEventSubscriptions } from './relay-event-subscriptions-ui.js';
|
|
40
40
|
import {
|
|
41
41
|
upsertRelayQuestion,
|
|
42
42
|
loadRelayQuestions,
|
|
@@ -45,7 +45,10 @@ import {
|
|
|
45
45
|
import { upsertRelayBoard, loadRelayBoards, renderRelayBoards } from './relay-board-view.js';
|
|
46
46
|
import { setConversationBackgroundTasks } from './background-tasks-view.mjs';
|
|
47
47
|
import { setConversationPins } from './pinned-messages-view.mjs';
|
|
48
|
+
import { setConversationHiddenMessages } from './hidden-messages-view.mjs';
|
|
48
49
|
import { setPreviews } from './preview-cards.mjs';
|
|
50
|
+
import { applyShareSettings, hasActiveShare, refreshShares } from './share-state.js';
|
|
51
|
+
import { handleShareOpened, handleViewerIpsForgotten } from './shared-conversations-settings.js';
|
|
49
52
|
import { setRemoteRelaysSnapshot } from './remote-relays-store.mjs';
|
|
50
53
|
import { REMOTE_RELAY_SOCKET_EVENT } from './remote-relay-shared.mjs';
|
|
51
54
|
import { AGENT_SESSIONS_SOCKET_EVENT } from './agent-sessions-settings.mjs';
|
|
@@ -54,6 +57,7 @@ import { applyClaudeAuthState } from './claude-auth-ui.js';
|
|
|
54
57
|
import { applyGrokAuthState } from './grok-auth-ui.js';
|
|
55
58
|
import { applyClaudeCloudSettingsState, refreshClaudeCloudSettingsState } from './claude-cloud-settings-ui.js';
|
|
56
59
|
import { applyClaudeCloudSessionEvent } from './claude-cloud-conversation-ui.js';
|
|
60
|
+
import { updateProviderReadinessInputs } from './provider-readiness.mjs';
|
|
57
61
|
import { applyCliInstallState } from './cli-install-ui.js';
|
|
58
62
|
import { renderUpdateSection } from './update-ui.js';
|
|
59
63
|
import { applyHostSuspendState, applyRelayShutdownState } from './host-suspend-ui.js';
|
|
@@ -73,7 +77,10 @@ import {
|
|
|
73
77
|
applyConversationTurnStatus,
|
|
74
78
|
renderMessages,
|
|
75
79
|
appendMessage,
|
|
80
|
+
captureLiveReplyReadingPosition,
|
|
81
|
+
restoreLiveReplyReadingPosition,
|
|
76
82
|
replaceRenderedMessage,
|
|
83
|
+
noteTranscriptLiveRow,
|
|
77
84
|
applyIncomingConversationDraftUpdate,
|
|
78
85
|
getRenderedConversationMessageFingerprints,
|
|
79
86
|
clearBubbleCancelState,
|
|
@@ -83,11 +90,18 @@ import {
|
|
|
83
90
|
syncComposerButtonState,
|
|
84
91
|
syncCancellableSteerButtons,
|
|
85
92
|
} from './conversation-view.js';
|
|
93
|
+
import { followsTranscriptEnd, noteTranscriptNews } from './transcript-jump-bottom-view.mjs';
|
|
86
94
|
import { loadRepoBrowserTree, refreshRepoBrowserIfWorkspaceOpen } from './attachments-view.js';
|
|
87
95
|
import { clearMessageSearchRuntimeState } from './message-search-view.js';
|
|
88
96
|
import { stripRelayPromptContext } from './relay-prompt-sanitizer.mjs';
|
|
89
97
|
import { isLikelyLiveDuplicateMessage } from './live-message-dedupe.mjs';
|
|
90
98
|
import { mergeRelayThoughts } from './relay-thoughts.mjs';
|
|
99
|
+
import {
|
|
100
|
+
handleSubagentStatusEventForViewer,
|
|
101
|
+
handleSubagentTimelineEvent,
|
|
102
|
+
handleSubagentViewerReconnect,
|
|
103
|
+
refreshSubagentRows,
|
|
104
|
+
} from './subagent-viewer.mjs';
|
|
91
105
|
|
|
92
106
|
const FALLBACK_MODE = 'agent';
|
|
93
107
|
|
|
@@ -325,8 +339,17 @@ export async function connectSocket(overrideDeps) {
|
|
|
325
339
|
setCliOnline(true);
|
|
326
340
|
renderConvList();
|
|
327
341
|
resyncAfterConnect();
|
|
342
|
+
// Every loaded page again, not page one only: events missed while the
|
|
343
|
+
// socket was down touched rows anywhere in the list.
|
|
344
|
+
scheduleLoadedConversationsRefresh();
|
|
328
345
|
refreshSessionWorkerStatus().catch(() => {});
|
|
329
346
|
refreshModelCatalog().catch(() => {});
|
|
347
|
+
// An open sub-agent viewer fetches whatever it missed while away.
|
|
348
|
+
handleSubagentViewerReconnect();
|
|
349
|
+
// Subscriptions that changed while away.
|
|
350
|
+
void loadRelayEventSubscriptions();
|
|
351
|
+
// Shares made or stopped while away.
|
|
352
|
+
void refreshShares().catch(() => {});
|
|
330
353
|
});
|
|
331
354
|
socket.on('connect_error', (e) => {
|
|
332
355
|
setRelayOnline(false);
|
|
@@ -357,8 +380,11 @@ export async function connectSocket(overrideDeps) {
|
|
|
357
380
|
updateModelCatalogState(payload || {});
|
|
358
381
|
void reconcileOpenModelVariantModal();
|
|
359
382
|
});
|
|
383
|
+
// Each settings event also feeds provider readiness (provider-readiness.mjs),
|
|
384
|
+
// so a Settings save changes the empty screen without a reload.
|
|
360
385
|
socket.on('openai_settings_updated', (payload) => {
|
|
361
|
-
deps?.applyOpenAISettingsState?.(payload || {});
|
|
386
|
+
const state = deps?.applyOpenAISettingsState?.(payload || {});
|
|
387
|
+
updateProviderReadinessInputs({ openai: state || payload || null });
|
|
362
388
|
if (Number(payload?.reconciliation?.updatedUnstartedConversations || 0) > 0) {
|
|
363
389
|
void Promise.resolve()
|
|
364
390
|
.then(() => deps?.refreshCurrentView?.())
|
|
@@ -371,7 +397,8 @@ export async function connectSocket(overrideDeps) {
|
|
|
371
397
|
deps?.applyCopilotSettingsState?.(payload || {});
|
|
372
398
|
});
|
|
373
399
|
socket.on('claude_settings_updated', (payload) => {
|
|
374
|
-
deps?.applyClaudeSettingsState?.(payload || {});
|
|
400
|
+
const state = deps?.applyClaudeSettingsState?.(payload || {});
|
|
401
|
+
updateProviderReadinessInputs({ claude: state || payload || null });
|
|
375
402
|
if (Number(payload?.reconciliation?.updatedUnstartedConversations || 0) > 0) {
|
|
376
403
|
void Promise.resolve()
|
|
377
404
|
.then(() => deps?.refreshCurrentView?.())
|
|
@@ -421,8 +448,13 @@ export async function connectSocket(overrideDeps) {
|
|
|
421
448
|
// carries the same shape as GET /api/settings/claude-cloud; one without a
|
|
422
449
|
// body is read as "something changed" and answered with a fresh read.
|
|
423
450
|
socket.on('claude_cloud_settings_updated', (payload) => {
|
|
424
|
-
if (payload && typeof payload === 'object')
|
|
425
|
-
|
|
451
|
+
if (payload && typeof payload === 'object') {
|
|
452
|
+
updateProviderReadinessInputs({ claudeCloud: applyClaudeCloudSettingsState(payload) || null });
|
|
453
|
+
} else {
|
|
454
|
+
void refreshClaudeCloudSettingsState().then((state) => {
|
|
455
|
+
updateProviderReadinessInputs({ claudeCloud: state || null });
|
|
456
|
+
});
|
|
457
|
+
}
|
|
426
458
|
});
|
|
427
459
|
// A cloud session got its id, pushed a branch or reported a new cost: the
|
|
428
460
|
// conversation's `cloud` field changed, and with it the cloud line.
|
|
@@ -430,7 +462,8 @@ export async function connectSocket(overrideDeps) {
|
|
|
430
462
|
if (applyClaudeCloudSessionEvent(payload || null)) deps?.syncChatTitleControls?.();
|
|
431
463
|
});
|
|
432
464
|
socket.on('grok_settings_updated', (payload) => {
|
|
433
|
-
deps?.applyGrokSettingsState?.(payload || {});
|
|
465
|
+
const state = deps?.applyGrokSettingsState?.(payload || {});
|
|
466
|
+
updateProviderReadinessInputs({ grok: state || payload || null });
|
|
434
467
|
if (Number(payload?.reconciliation?.updatedUnstartedConversations || 0) > 0) {
|
|
435
468
|
void Promise.resolve()
|
|
436
469
|
.then(() => deps?.refreshCurrentView?.())
|
|
@@ -438,7 +471,8 @@ export async function connectSocket(overrideDeps) {
|
|
|
438
471
|
}
|
|
439
472
|
});
|
|
440
473
|
socket.on('cursor_settings_updated', (payload) => {
|
|
441
|
-
deps?.applyCursorSettingsState?.(payload || {});
|
|
474
|
+
const state = deps?.applyCursorSettingsState?.(payload || {});
|
|
475
|
+
updateProviderReadinessInputs({ cursor: state || payload || null });
|
|
442
476
|
if (Number(payload?.reconciliation?.updatedUnstartedConversations || 0) > 0) {
|
|
443
477
|
void Promise.resolve()
|
|
444
478
|
.then(() => deps?.refreshCurrentView?.())
|
|
@@ -485,12 +519,19 @@ export async function connectSocket(overrideDeps) {
|
|
|
485
519
|
})) {
|
|
486
520
|
return;
|
|
487
521
|
}
|
|
488
|
-
|
|
522
|
+
// Another client's message (an agent's prompt included) follows the
|
|
523
|
+
// end like any live row: a reader scrolled up stays where they are,
|
|
524
|
+
// and the jump-to-bottom badge counts it.
|
|
525
|
+
appendMessage(normalizedMessage, followsTranscriptEnd(), messageId);
|
|
526
|
+
noteTranscriptLiveRow();
|
|
489
527
|
}
|
|
490
528
|
});
|
|
491
529
|
socket.on('assistant_message', ({ conversationId, message, messageId, sourceMessageId }) => {
|
|
492
530
|
const isCurrentConversation = conversationId === currentConvId;
|
|
493
|
-
const autoScroll = isCurrentConversation ?
|
|
531
|
+
const autoScroll = isCurrentConversation ? followsTranscriptEnd() : false;
|
|
532
|
+
// A reader who is not at the end keeps what they read where it is while
|
|
533
|
+
// the reply takes the live bubble's place.
|
|
534
|
+
const reading = isCurrentConversation && !autoScroll ? captureLiveReplyReadingPosition() : null;
|
|
494
535
|
// A background conversation finishing its turn must not wipe the viewed
|
|
495
536
|
// conversation's live bubble.
|
|
496
537
|
if (isCurrentConversation) {
|
|
@@ -509,9 +550,14 @@ export async function connectSocket(overrideDeps) {
|
|
|
509
550
|
message.thoughts = mergedThoughts;
|
|
510
551
|
}
|
|
511
552
|
}
|
|
512
|
-
if (messageId && seenMessageIds?.has(messageId))
|
|
553
|
+
if (messageId && seenMessageIds?.has(messageId)) {
|
|
554
|
+
restoreLiveReplyReadingPosition(reading);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
513
557
|
if (isCurrentConversation) {
|
|
514
558
|
appendMessage(message, autoScroll, messageId || null, false, sourceMessageId || null);
|
|
559
|
+
restoreLiveReplyReadingPosition(reading);
|
|
560
|
+
noteTranscriptLiveRow();
|
|
515
561
|
scheduleContextUsageRefresh(conversationId, 120);
|
|
516
562
|
}
|
|
517
563
|
if (sourceMessageId) relayActivities.delete(sourceMessageId);
|
|
@@ -530,8 +576,9 @@ export async function connectSocket(overrideDeps) {
|
|
|
530
576
|
socket.on('message_updated', ({ conversationId, messageId, message }) => {
|
|
531
577
|
if (!messageId || conversationId !== currentConvId || !message) return;
|
|
532
578
|
if (!replaceRenderedMessage(message, messageId)) {
|
|
533
|
-
appendMessage(message,
|
|
579
|
+
appendMessage(message, followsTranscriptEnd(), messageId, false);
|
|
534
580
|
}
|
|
581
|
+
noteTranscriptLiveRow();
|
|
535
582
|
});
|
|
536
583
|
socket.on('relay_question', ({ question }) => upsertRelayQuestion(question));
|
|
537
584
|
socket.on('relay_question_updated', ({ question }) => upsertRelayQuestion(question));
|
|
@@ -543,19 +590,55 @@ export async function connectSocket(overrideDeps) {
|
|
|
543
590
|
socket.on('relay_board_changed', () => {
|
|
544
591
|
loadRelayBoards();
|
|
545
592
|
});
|
|
593
|
+
// Relay event subscriptions: the whole active list, after every change.
|
|
594
|
+
socket.on('relay_event_subscriptions', ({ subscriptions }) => {
|
|
595
|
+
applyRelayEventSubscriptions(subscriptions);
|
|
596
|
+
});
|
|
546
597
|
socket.on('background_tasks', ({ conversationId, tasks }) => {
|
|
547
598
|
setConversationBackgroundTasks(conversationId, tasks);
|
|
599
|
+
// The sidebar's row (and its group's head) count background work as
|
|
600
|
+
// running; the set arrives under the conversation or its session id.
|
|
601
|
+
const id = String(conversationId || '').trim();
|
|
602
|
+
const record = conversations[id]
|
|
603
|
+
|| Object.values(conversations).find((conversation) => String(conversation?.sdkSessionId || '') === id)
|
|
604
|
+
|| null;
|
|
605
|
+
if (record && applyBackgroundTasksToConversation(record, tasks)) renderConvList();
|
|
548
606
|
});
|
|
549
607
|
// A message was pinned or unpinned (any client): the conversation's whole
|
|
550
608
|
// pin list, replacing what this page had.
|
|
551
609
|
socket.on('conversation_pins_updated', ({ conversationId, pins, revision }) => {
|
|
552
610
|
setConversationPins(conversationId, pins, revision);
|
|
553
611
|
});
|
|
612
|
+
// A message was hidden from or shown to shared viewers (any client): the
|
|
613
|
+
// conversation's whole hidden list, replacing what this page had.
|
|
614
|
+
socket.on('conversation_hidden_messages_updated', ({ conversationId, hiddenMessages, revision }) => {
|
|
615
|
+
setConversationHiddenMessages(conversationId, hiddenMessages, revision);
|
|
616
|
+
});
|
|
554
617
|
// REPLACE semantics for the whole set: the preview registry is relay-owned
|
|
555
618
|
// and global, so every change ships the full list.
|
|
556
619
|
socket.on('previews', ({ previews }) => {
|
|
557
620
|
setPreviews(previews);
|
|
558
621
|
});
|
|
622
|
+
// Public shares: the events name the conversation only; the list (Settings
|
|
623
|
+
// → Sharing, the ⋯ menu's Stop sharing) is read again.
|
|
624
|
+
for (const event of ['share_created', 'share_renewed', 'share_revoked']) {
|
|
625
|
+
socket.on(event, () => {
|
|
626
|
+
void refreshShares().catch(() => {});
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
// A deleted conversation takes its share along.
|
|
630
|
+
socket.on('conversation_deleted', ({ conversationId } = {}) => {
|
|
631
|
+
if (hasActiveShare(conversationId)) void refreshShares().catch(() => {});
|
|
632
|
+
});
|
|
633
|
+
socket.on('share_opened', ({ conversationId } = {}) => {
|
|
634
|
+
handleShareOpened(conversationId);
|
|
635
|
+
});
|
|
636
|
+
socket.on('share_settings_changed', (payload) => {
|
|
637
|
+
applyShareSettings(payload || null);
|
|
638
|
+
});
|
|
639
|
+
socket.on('share_viewer_ips_forgotten', () => {
|
|
640
|
+
handleViewerIpsForgotten();
|
|
641
|
+
});
|
|
559
642
|
// REPLACE semantics too: the relay ships the whole remote-relay list (no
|
|
560
643
|
// tokens) whenever a remote is added, removed, renamed or changes status.
|
|
561
644
|
socket.on(REMOTE_RELAY_SOCKET_EVENT, (payload) => {
|
|
@@ -580,16 +663,17 @@ export async function connectSocket(overrideDeps) {
|
|
|
580
663
|
const last = items[items.length - 1];
|
|
581
664
|
const lastText = typeof last === 'string' ? last : String(last?.text || '');
|
|
582
665
|
const lastSubagentRunId = typeof last === 'object' && last ? (last.subagentRunId || null) : null;
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
666
|
+
// The same line again (a repeated event) adds nothing.
|
|
667
|
+
const added = lastText !== entry.text || lastSubagentRunId !== entry.subagentRunId;
|
|
668
|
+
if (added) relayActivities.set(messageId, capLiveActivityEntries(items.concat(entry)));
|
|
586
669
|
if (entry.subagentRunId) {
|
|
587
670
|
upsertSubagentRun({ subagentRunId: entry.subagentRunId, messageId, conversationId });
|
|
588
671
|
addSubagentActivity(entry.subagentRunId, entry.text);
|
|
589
672
|
}
|
|
590
673
|
if (conversationId === currentConvId) {
|
|
591
|
-
const autoScroll =
|
|
674
|
+
const autoScroll = followsTranscriptEnd();
|
|
592
675
|
appendThinkingActivity(entry, entry.subagentRunId, autoScroll);
|
|
676
|
+
if (added && !autoScroll) noteTranscriptNews();
|
|
593
677
|
}
|
|
594
678
|
});
|
|
595
679
|
socket.on('relay_stream', ({ conversationId, messageId, text, done, seq, subagentRunId }) => {
|
|
@@ -598,8 +682,8 @@ export async function connectSocket(overrideDeps) {
|
|
|
598
682
|
upsertSubagentRun({ subagentRunId: String(subagentRunId).trim(), messageId, conversationId });
|
|
599
683
|
}
|
|
600
684
|
if (conversationId !== currentConvId) return;
|
|
601
|
-
const autoScroll =
|
|
602
|
-
applyRelayStreamEvent({
|
|
685
|
+
const autoScroll = followsTranscriptEnd();
|
|
686
|
+
const accepted = applyRelayStreamEvent({
|
|
603
687
|
messageId,
|
|
604
688
|
text: String(text || ''),
|
|
605
689
|
done: !!done,
|
|
@@ -607,11 +691,16 @@ export async function connectSocket(overrideDeps) {
|
|
|
607
691
|
subagentRunId: subagentRunId ? String(subagentRunId).trim() : null,
|
|
608
692
|
autoScroll,
|
|
609
693
|
});
|
|
694
|
+
// A frame that was dropped (out of order, a finished turn) is no news.
|
|
695
|
+
if (accepted && !autoScroll) noteTranscriptNews();
|
|
610
696
|
});
|
|
611
697
|
socket.on('relay_thought', ({ conversationId, messageId, reasoningId, text, done, subagentRunId }) => {
|
|
612
698
|
if (!messageId) return;
|
|
613
699
|
const key = String(reasoningId || 'reasoning');
|
|
614
700
|
const thoughtMap = relayThoughts.get(messageId) || new Map();
|
|
701
|
+
const known = thoughtMap.get(key);
|
|
702
|
+
// The same thought again (a repeated event) adds nothing.
|
|
703
|
+
const added = !known || known.text !== String(text || '') || known.done !== !!done;
|
|
615
704
|
thoughtMap.set(key, { reasoningId: key, text: String(text || ''), done: !!done, subagentRunId: subagentRunId || null });
|
|
616
705
|
relayThoughts.set(messageId, thoughtMap);
|
|
617
706
|
if (subagentRunId) {
|
|
@@ -619,28 +708,33 @@ export async function connectSocket(overrideDeps) {
|
|
|
619
708
|
addSubagentThought(subagentRunId, { reasoningId: key, text: String(text || ''), done: !!done });
|
|
620
709
|
}
|
|
621
710
|
if (conversationId === currentConvId) {
|
|
622
|
-
const autoScroll =
|
|
711
|
+
const autoScroll = followsTranscriptEnd();
|
|
623
712
|
appendThinkingThought(key, String(text || ''), !!done, subagentRunId, autoScroll);
|
|
713
|
+
if (added && !autoScroll) noteTranscriptNews();
|
|
624
714
|
}
|
|
625
715
|
});
|
|
626
|
-
socket.on('subagent_status', (
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
status,
|
|
635
|
-
timestamp,
|
|
636
|
-
});
|
|
716
|
+
socket.on('subagent_status', (payload = {}) => {
|
|
717
|
+
const { conversationId, subagentRunId, status, stopUnsupported } = payload;
|
|
718
|
+
if (!subagentRunId) return;
|
|
719
|
+
// An event without its turn is placed on the run the store already
|
|
720
|
+
// knows; one for an unknown run without a turn cannot be placed.
|
|
721
|
+
const messageId = payload.messageId || getSubagentRun(subagentRunId)?.messageId || null;
|
|
722
|
+
if (!messageId) return;
|
|
723
|
+
upsertSubagentRun({ ...payload, messageId });
|
|
637
724
|
clearSubagentCancelInFlight(subagentRunId);
|
|
638
725
|
// The provider answered "not supported": pin the state so the button does
|
|
639
726
|
// not re-arm into an endless click-and-fail loop.
|
|
640
727
|
if (stopUnsupported) markSubagentStopUnsupported(subagentRunId);
|
|
641
728
|
if (conversationId === currentConvId) {
|
|
642
729
|
updateSubagentBubbleFromStatus(subagentRunId, status);
|
|
730
|
+
refreshSubagentRows(subagentRunId);
|
|
643
731
|
}
|
|
732
|
+
handleSubagentStatusEventForViewer({ ...payload, messageId });
|
|
733
|
+
});
|
|
734
|
+
// Entries of a sub-agent's record; the relay sends them only while this
|
|
735
|
+
// page's viewer watches that run.
|
|
736
|
+
socket.on('subagent_timeline', (payload) => {
|
|
737
|
+
handleSubagentTimelineEvent(payload);
|
|
644
738
|
});
|
|
645
739
|
socket.on('conversation_compacted', async ({ sourceConversationId, targetConversationId }) => {
|
|
646
740
|
if (!sourceConversationId || !targetConversationId) return;
|
|
@@ -733,6 +827,9 @@ export async function connectSocket(overrideDeps) {
|
|
|
733
827
|
applyConversationTurnStatus({ conversationId, messageId, status });
|
|
734
828
|
if (conversationId && conversations[conversationId]) {
|
|
735
829
|
const conversation = conversations[conversationId];
|
|
830
|
+
// The row's turn fields follow the event, open or not: the dots, a
|
|
831
|
+
// group head's pill and its fold read them between list fetches.
|
|
832
|
+
applyTurnStatusToConversation(conversation, normalizedStatus);
|
|
736
833
|
if (normalizedStatus === 'processing') {
|
|
737
834
|
conversation.localTurnStatus = 'processing';
|
|
738
835
|
conversation.localTurnStatusUpdatedAt = Date.now();
|
|
@@ -748,7 +845,7 @@ export async function connectSocket(overrideDeps) {
|
|
|
748
845
|
}
|
|
749
846
|
}
|
|
750
847
|
if (conversationId === currentConvId && normalizedStatus === 'processing') {
|
|
751
|
-
const autoScroll =
|
|
848
|
+
const autoScroll = followsTranscriptEnd();
|
|
752
849
|
showThinking(messageId || null, autoScroll);
|
|
753
850
|
renderThinkingActivities();
|
|
754
851
|
renderThinkingThoughts();
|
|
@@ -784,6 +881,12 @@ export async function connectSocket(overrideDeps) {
|
|
|
784
881
|
}
|
|
785
882
|
renderConvList();
|
|
786
883
|
});
|
|
884
|
+
// A session group changed (a member joined or left, a name, a dissolve,
|
|
885
|
+
// here or from another device): every loaded page is fetched again with
|
|
886
|
+
// its groups, so a row far down the list changes too.
|
|
887
|
+
socket.on('conversation_group_changed', (payload) => {
|
|
888
|
+
applyConversationGroupEvent(payload);
|
|
889
|
+
});
|
|
787
890
|
socket.on('conversation_archived', ({ conversationId, archived }) => {
|
|
788
891
|
const id = String(conversationId || '').trim();
|
|
789
892
|
if (!id) return;
|
|
@@ -811,13 +914,7 @@ export async function connectSocket(overrideDeps) {
|
|
|
811
914
|
renderRelayBoards();
|
|
812
915
|
renderConvList();
|
|
813
916
|
if (currentConvId === conversationId) {
|
|
814
|
-
|
|
815
|
-
renderMessages([]);
|
|
816
|
-
document.getElementById('chat-title').textContent = 'Select or start a conversation';
|
|
817
|
-
syncChatTitleControls();
|
|
818
|
-
updateSessionPill(null, null);
|
|
819
|
-
updateCompactButton();
|
|
820
|
-
scheduleContextUsageRefresh(null);
|
|
917
|
+
clearOpenConversation();
|
|
821
918
|
} else {
|
|
822
919
|
updateCompactButton();
|
|
823
920
|
}
|
|
@@ -5,6 +5,7 @@ import { readRepoBrowserPreferences } from './repo-browser-preferences.mjs';
|
|
|
5
5
|
import { isChatInteractionHeld } from './selection-guard.mjs';
|
|
6
6
|
import { nextSubagentStatus } from './subagent-fold.mjs';
|
|
7
7
|
import { useStrictStrikethrough } from './markdown-strikethrough.mjs';
|
|
8
|
+
import { parseGroupFoldChoices, pruneGroupFoldChoices } from './conversation-group-state.mjs';
|
|
8
9
|
|
|
9
10
|
function resolveAppBase() {
|
|
10
11
|
const configuredBase = typeof window.__COPILOT_APP_CONFIG?.basePath === 'string'
|
|
@@ -16,6 +17,8 @@ function resolveAppBase() {
|
|
|
16
17
|
const pathname = String(window.location.pathname || '/');
|
|
17
18
|
const sharedIndex = pathname.indexOf('/shared/');
|
|
18
19
|
if (sharedIndex >= 0) return pathname.slice(0, sharedIndex).replace(/\/+$/, '');
|
|
20
|
+
const previewIndex = pathname.indexOf('/share-preview/');
|
|
21
|
+
if (previewIndex >= 0) return pathname.slice(0, previewIndex).replace(/\/+$/, '');
|
|
19
22
|
return pathname.replace(/\/+$/, '');
|
|
20
23
|
}
|
|
21
24
|
|
|
@@ -32,8 +35,24 @@ function resolveSharedConversationToken() {
|
|
|
32
35
|
if (configured) return configured;
|
|
33
36
|
return extractSharedTokenFromPathname(window.location.pathname);
|
|
34
37
|
}
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
// "Preview as shared": the owner reads a conversation as a share link would
|
|
39
|
+
// serve it (marked redactions), through the owner-only preview routes. It is a
|
|
40
|
+
// shared view in every other respect.
|
|
41
|
+
function resolveSharePreviewConversationId() {
|
|
42
|
+
const configured = String(window.__COPILOT_APP_CONFIG?.sharePreviewConversationId || '').trim();
|
|
43
|
+
if (configured) return configured;
|
|
44
|
+
const match = String(window.location.pathname || '/').match(/\/share-preview\/([^/?#]+)\/?$/i);
|
|
45
|
+
if (!match) return '';
|
|
46
|
+
try {
|
|
47
|
+
return decodeURIComponent(String(match[1] || '')).trim();
|
|
48
|
+
} catch {
|
|
49
|
+
return '';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export const SHARE_PREVIEW_CONVERSATION_ID = resolveSharePreviewConversationId();
|
|
53
|
+
export const IS_SHARE_PREVIEW = !!SHARE_PREVIEW_CONVERSATION_ID;
|
|
54
|
+
export const SHARED_CONVERSATION_TOKEN = IS_SHARE_PREVIEW ? '' : resolveSharedConversationToken();
|
|
55
|
+
export const IS_SHARED_VIEW = !!SHARED_CONVERSATION_TOKEN || IS_SHARE_PREVIEW;
|
|
37
56
|
export let TOKEN = '';
|
|
38
57
|
export let socket = null;
|
|
39
58
|
export let currentConvId = null;
|
|
@@ -82,6 +101,9 @@ export let updateState = null;
|
|
|
82
101
|
export let activeRuntimeSessionCount = 0;
|
|
83
102
|
export let runtimeSessionBindingCount = 0;
|
|
84
103
|
export let conversations = {};
|
|
104
|
+
// Session groups by id, as the list payload describes them (`groups`):
|
|
105
|
+
// name, display name, head and member counts. Merged with every page.
|
|
106
|
+
export const conversationGroups = {};
|
|
85
107
|
export let selectedAttachments = [];
|
|
86
108
|
export let imageEditTarget = null;
|
|
87
109
|
export const RELAY_QUESTION_POLL_MS = 3000;
|
|
@@ -172,6 +194,8 @@ const SIDEBAR_WIDTH_PERCENT_FALLBACK = 24;
|
|
|
172
194
|
const SIDEBAR_WIDTH_PORTRAIT_STORAGE_KEY = 'copilot_sidebar_width_pct_portrait';
|
|
173
195
|
const SIDEBAR_WIDTH_LANDSCAPE_STORAGE_KEY = 'copilot_sidebar_width_pct_landscape';
|
|
174
196
|
const SIDEBAR_COLLAPSED_DESKTOP_STORAGE_KEY = 'copilot_sidebar_collapsed_desktop';
|
|
197
|
+
// The user's fold choices for session groups in the sidebar: { [groupId]: 'open' | 'folded' }.
|
|
198
|
+
const CONVERSATION_GROUP_FOLDS_STORAGE_KEY = 'copilot_conv_group_folds';
|
|
175
199
|
let sidebarWidthPercent = SIDEBAR_WIDTH_PERCENT_FALLBACK;
|
|
176
200
|
const CONVERSATION_SCROLL_STORAGE_PREFIX = 'copilot_message_scroll_';
|
|
177
201
|
|
|
@@ -197,6 +221,59 @@ export function setCurrentConv(id) {
|
|
|
197
221
|
updateCompactButton();
|
|
198
222
|
}
|
|
199
223
|
|
|
224
|
+
/** Merges a list payload's `groups` (by id) into the known session groups. */
|
|
225
|
+
export function mergeConversationGroups(groups) {
|
|
226
|
+
if (!groups || typeof groups !== 'object') return;
|
|
227
|
+
for (const [groupId, group] of Object.entries(groups)) {
|
|
228
|
+
const id = String(groupId || '').trim();
|
|
229
|
+
if (!id || !group || typeof group !== 'object') continue;
|
|
230
|
+
conversationGroups[id] = { ...(conversationGroups[id] || {}), ...group, id };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Forgets a session group the relay no longer lists. */
|
|
235
|
+
export function forgetConversationGroup(groupId) {
|
|
236
|
+
delete conversationGroups[String(groupId || '').trim()];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Keeps only the groups that a loaded row still belongs to: after a list
|
|
241
|
+
* refresh a dissolved group has no row left, and the picker must not offer
|
|
242
|
+
* it. Returns the ids dropped.
|
|
243
|
+
*/
|
|
244
|
+
export function pruneConversationGroups(keepGroupIds) {
|
|
245
|
+
const keep = new Set((Array.isArray(keepGroupIds) ? keepGroupIds : []).map((id) => String(id || '').trim()).filter(Boolean));
|
|
246
|
+
const dropped = [];
|
|
247
|
+
for (const id of Object.keys(conversationGroups)) {
|
|
248
|
+
if (keep.has(id)) continue;
|
|
249
|
+
delete conversationGroups[id];
|
|
250
|
+
dropped.push(id);
|
|
251
|
+
}
|
|
252
|
+
return dropped;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function readConversationGroupFoldChoices() {
|
|
256
|
+
return parseGroupFoldChoices(localStorage.getItem(CONVERSATION_GROUP_FOLDS_STORAGE_KEY) || '');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// The stored fold choices are trimmed to the loaded groups only past this
|
|
260
|
+
// size: a choice for a group on an older page or in the other view must
|
|
261
|
+
// not go with every toggle.
|
|
262
|
+
const CONVERSATION_GROUP_FOLDS_MAX = 300;
|
|
263
|
+
|
|
264
|
+
/** Stores the user's fold choice for a group. */
|
|
265
|
+
export function setConversationGroupFoldChoice(groupId, choice) {
|
|
266
|
+
const id = String(groupId || '').trim();
|
|
267
|
+
if (!id) return;
|
|
268
|
+
let next = readConversationGroupFoldChoices();
|
|
269
|
+
if (Object.keys(next).length > CONVERSATION_GROUP_FOLDS_MAX) {
|
|
270
|
+
next = pruneGroupFoldChoices(next, [...Object.keys(conversationGroups), id]);
|
|
271
|
+
}
|
|
272
|
+
if (choice === 'open' || choice === 'folded') next[id] = choice;
|
|
273
|
+
else delete next[id];
|
|
274
|
+
localStorage.setItem(CONVERSATION_GROUP_FOLDS_STORAGE_KEY, JSON.stringify(next));
|
|
275
|
+
}
|
|
276
|
+
|
|
200
277
|
export function setConversationWatcherCount(conversationId, count) {
|
|
201
278
|
const id = String(conversationId || '').trim();
|
|
202
279
|
if (!id) return;
|
|
@@ -612,37 +689,18 @@ export function getMessagesDistanceFromBottom() {
|
|
|
612
689
|
return Math.max(0, el.scrollHeight - el.clientHeight - el.scrollTop);
|
|
613
690
|
}
|
|
614
691
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
export function isMessagesNearBottom(
|
|
623
|
-
const el = document.getElementById('messages');
|
|
624
|
-
if (!el) return true;
|
|
625
|
-
const distance = Math.max(0, el.scrollHeight - el.clientHeight - el.scrollTop);
|
|
626
|
-
const threshold = resolveMessagesNearBottomThreshold(el, thresholdPx);
|
|
627
|
-
return distance <= threshold;
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
export function isMessagesAtBottom(epsilonPx = 1) {
|
|
692
|
+
/**
|
|
693
|
+
* "At the end" of the transcript, wherever that is decided: within the
|
|
694
|
+
* near-bottom margin (8% of the viewport, between 12 and 48 px). One margin
|
|
695
|
+
* and no parameter: a caller with a margin of its own (there was one with
|
|
696
|
+
* 80 px, one with 1 px, and a default that read as 0 px) breaks the rule that
|
|
697
|
+
* the transcript follows a reader who is at the end and nobody else.
|
|
698
|
+
*/
|
|
699
|
+
export function isMessagesNearBottom() {
|
|
631
700
|
const el = document.getElementById('messages');
|
|
632
701
|
if (!el) return true;
|
|
633
|
-
const epsilon = Math.max(0, Number.isFinite(Number(epsilonPx)) ? Number(epsilonPx) : 1);
|
|
634
702
|
const distance = Math.max(0, el.scrollHeight - el.clientHeight - el.scrollTop);
|
|
635
|
-
return distance <=
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
export function scrollBottomAfterSend() {
|
|
639
|
-
scrollBottom();
|
|
640
|
-
requestAnimationFrame(() => {
|
|
641
|
-
scrollBottom();
|
|
642
|
-
});
|
|
643
|
-
setTimeout(() => {
|
|
644
|
-
scrollBottom();
|
|
645
|
-
}, 120);
|
|
703
|
+
return distance <= resolveNearBottomThresholdPx(el.clientHeight);
|
|
646
704
|
}
|
|
647
705
|
|
|
648
706
|
export function isMobileComposerViewport() {
|
|
@@ -944,8 +1002,16 @@ function buildSessionWorkerStateHash(map) {
|
|
|
944
1002
|
|
|
945
1003
|
let sessionWorkerStateHash = '';
|
|
946
1004
|
|
|
1005
|
+
// Whether a status poll has answered yet: before that, a session's reported
|
|
1006
|
+
// background work is taken as it is; after it, only a live worker carries it.
|
|
1007
|
+
let sessionWorkerStatusKnown = false;
|
|
1008
|
+
export function hasSessionWorkerStatus() {
|
|
1009
|
+
return sessionWorkerStatusKnown;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
947
1012
|
export function setSessionWorkerStatesFromStatusPayload(payload) {
|
|
948
1013
|
const workers = Array.isArray(payload?.workers) ? payload.workers : [];
|
|
1014
|
+
sessionWorkerStatusKnown = true;
|
|
949
1015
|
const next = new Map();
|
|
950
1016
|
for (const worker of workers) {
|
|
951
1017
|
const normalized = normalizeWorkerStateEntry(worker);
|
|
@@ -1498,6 +1564,17 @@ export function isSubagentCancelInFlight(subagentRunId) {
|
|
|
1498
1564
|
return id ? subagentCancelInFlight.has(id) : false;
|
|
1499
1565
|
}
|
|
1500
1566
|
|
|
1567
|
+
const SUBAGENT_HEADER_FIELDS = ['completedAt', 'model', 'modelInherited', 'subagentType', 'taskId', 'totalTokens', 'background', 'truncated', 'stoppable'];
|
|
1568
|
+
|
|
1569
|
+
function subagentHeaderFields(payload, existing) {
|
|
1570
|
+
const fields = {};
|
|
1571
|
+
for (const name of SUBAGENT_HEADER_FIELDS) {
|
|
1572
|
+
const value = payload?.[name];
|
|
1573
|
+
fields[name] = value === undefined ? (existing?.[name] ?? null) : value;
|
|
1574
|
+
}
|
|
1575
|
+
return fields;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1501
1578
|
export function upsertSubagentRun(payload) {
|
|
1502
1579
|
const subagentRunId = normalizeSubagentRunId(payload?.subagentRunId);
|
|
1503
1580
|
if (!subagentRunId) return null;
|
|
@@ -1511,8 +1588,14 @@ export function upsertSubagentRun(payload) {
|
|
|
1511
1588
|
parentSubagentId: normalizeSubagentRunId(payload?.parentSubagentId) || existing?.parentSubagentId || null,
|
|
1512
1589
|
displayName: String(payload?.displayName || existing?.displayName || '').trim() || null,
|
|
1513
1590
|
status: nextSubagentStatus({ reported: payload?.status, known: existing?.status, normalize: normalizeSubagentStatus }),
|
|
1514
|
-
startedAt: existing?.startedAt || payload?.timestamp || now,
|
|
1515
|
-
updatedAt: payload?.timestamp || now,
|
|
1591
|
+
startedAt: payload?.startedAt || existing?.startedAt || payload?.timestamp || now,
|
|
1592
|
+
updatedAt: payload?.updatedAt || payload?.timestamp || now,
|
|
1593
|
+
// The run header (relay 0009): what the sub-agent rows and the viewer
|
|
1594
|
+
// show. An event that does not name a field keeps the known value.
|
|
1595
|
+
...subagentHeaderFields(payload, existing),
|
|
1596
|
+
// Whether `status` came from the relay rather than the default a live
|
|
1597
|
+
// lane's first frame starts with.
|
|
1598
|
+
statusReported: Boolean(String(payload?.status || '').trim()) || existing?.statusReported === true,
|
|
1516
1599
|
activities: existing?.activities || [],
|
|
1517
1600
|
thoughts: existing?.thoughts || [],
|
|
1518
1601
|
// Cumulative streamed text for this run, replayed when its bubble is
|