@oar-sh/oar 0.9.10 → 0.9.12
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 +440 -1
- package/README.md +156 -972
- package/package.json +13 -13
- package/server/README.md +112 -25
- package/server/claude-worker/claude-remote-relay-tool.mjs +2 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +57 -4
- package/server/claude-worker/claude-session-process.mjs +217 -11
- package/server/claude-worker/claude-session-worker.mjs +26 -1
- package/server/claude-worker/task-output-discovery.mjs +126 -0
- package/server/copilot-worker/copilot-sdk-session-process.mjs +235 -14
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +8 -0
- package/server/copilot-worker/copilot-shell-output-mirror.mjs +94 -0
- package/server/copilot-worker/copilot-task-settle.mjs +96 -0
- package/server/db-schema.mjs +68 -2
- package/server/grok-worker/acp-client.mjs +2 -1
- package/server/grok-worker/acp-host-services.mjs +46 -6
- package/server/grok-worker/grok-background-tasks.mjs +241 -0
- package/server/grok-worker/grok-sdk-adapter.mjs +3 -1
- package/server/grok-worker/grok-session-worker.mjs +33 -1
- package/server/grok-worker/grok-turn-runner.mjs +11 -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/migrations/0017-conversation-history-indexes.mjs +89 -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 +40 -101
- package/server/public/app/api-client.js +264 -16
- package/server/public/app/ask-user-view.js +17 -15
- package/server/public/app/background-tasks-view.mjs +285 -16
- package/server/public/app/bootstrap.js +253 -37
- package/server/public/app/chat-actions-menu-state.mjs +20 -10
- package/server/public/app/claude-background-command-limit-options.mjs +21 -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-payload-cache.mjs +357 -0
- package/server/public/app/conversation-turn-state.mjs +81 -0
- package/server/public/app/conversation-view.js +567 -94
- package/server/public/app/conversation-window-retention.mjs +96 -0
- package/server/public/app/cwd-picker.js +1 -1
- package/server/public/app/hidden-messages-view.mjs +312 -0
- package/server/public/app/host-suspend-ui.js +7 -1
- package/server/public/app/journal-view.js +1033 -82
- package/server/public/app/preview-cards.mjs +117 -31
- package/server/public/app/preview-command.mjs +10 -1
- package/server/public/app/relay-board-view.js +19 -6
- package/server/public/app/relay-prompt-sanitizer.mjs +11 -4
- package/server/public/app/relay-restart-error.mjs +16 -0
- package/server/public/app/settings-modal.js +150 -10
- 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 +138 -20
- package/server/public/app/store.js +121 -30
- package/server/public/app/subagent-row.mjs +3 -3
- package/server/public/app/subagent-viewer.mjs +37 -7
- package/server/public/app/task-console-render.mjs +237 -0
- package/server/public/app/task-console-watch.mjs +42 -0
- package/server/public/app/task-console.mjs +630 -0
- package/server/public/app/task-output-text.mjs +234 -0
- package/server/public/app/thinking-follow.js +0 -4
- package/server/public/app/transcript-island.mjs +73 -0
- package/server/public/app/transcript-jump-bottom-view.mjs +230 -26
- package/server/public/app/transcript-jump-bottom.mjs +170 -22
- package/server/public/app/transcript-reading-anchor.mjs +256 -0
- package/server/public/app/transcript-reload-freshness.mjs +17 -1
- package/server/public/app/transcript-viewport-anchor.mjs +159 -0
- package/server/public/index.html +625 -21
- package/server/relay-tools.md +3 -1
- package/server/relay.mjs +2 -1
- package/server/repositories/conversation-group-repository.mjs +444 -0
- package/server/repositories/message-repository.mjs +9 -0
- package/server/repositories/question-repository.mjs +4 -0
- 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/routes/about-routes.mjs +15 -0
- package/server/routes/cache-routes.mjs +4 -0
- package/server/routes/conversation-group-routes.mjs +171 -0
- package/server/routes/messages-routes.mjs +433 -68
- package/server/routes/preview-routes.mjs +9 -1
- package/server/routes/sessions-routes.mjs +901 -253
- package/server/server-runtime.mjs +392 -99
- package/server/services/about-service.mjs +461 -0
- package/server/services/background-task-store.mjs +365 -0
- package/server/services/background-work-view.mjs +228 -0
- package/server/services/cache-rebuild-service.mjs +30 -0
- package/server/services/cli-process-runner.mjs +4 -21
- package/server/services/conversation-history-page.mjs +199 -0
- package/server/services/delete-archive-service.mjs +4 -1
- package/server/services/detached-worker-watch-service.mjs +150 -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 +8 -4
- 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/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-service.mjs +17 -0
- package/server/services/relay-prompt-sanitizer.mjs +11 -4
- package/server/services/remote-relay-dispatcher.mjs +187 -7
- package/server/services/request-boundary.mjs +183 -0
- package/server/services/session-activity-tracker.mjs +231 -19
- 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/status-event-service.mjs +7 -3
- package/server/services/task-output-rooms.mjs +259 -0
- package/server/services/task-output-service.mjs +562 -0
- package/server/services/tunnel-worker-path-guard.mjs +24 -41
- package/server/services/windows-autostart-service.mjs +6 -1
- package/server/services/windows-boot-autostart-service.mjs +207 -23
- package/shared/claude-background-command-limit.mjs +83 -0
- package/shared/preview-tool-core.mjs +15 -5
- package/shared/remote-relay-contract.mjs +35 -3
- package/shared/share-path-privacy.mjs +30 -0
- package/shared/task-output-text.mjs +737 -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/task-output-spool.mjs +181 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +47 -15
|
@@ -20,7 +20,6 @@ import {
|
|
|
20
20
|
updateCompactButton,
|
|
21
21
|
clearPendingUserMessage,
|
|
22
22
|
hasPendingUserMessageDuplicate,
|
|
23
|
-
isMessagesAtBottom,
|
|
24
23
|
upsertSubagentRun,
|
|
25
24
|
getSubagentRun,
|
|
26
25
|
addSubagentActivity,
|
|
@@ -31,11 +30,13 @@ import {
|
|
|
31
30
|
applySessionWorkerSteering,
|
|
32
31
|
refreshSummaryModal,
|
|
33
32
|
summaryModalState,
|
|
33
|
+
IS_SHARED_VIEW,
|
|
34
34
|
} from './store.js';
|
|
35
35
|
import { scheduleContextUsageRefresh } from './api-client.js';
|
|
36
36
|
import { capLiveActivityEntries } from './activity-replay-state.mjs';
|
|
37
37
|
import { publishStatusEvent, recordStatusEvent } from './status-store.mjs';
|
|
38
|
-
import { clearOpenConversation, renderConvList, refreshConversations, openConversation } from './journal-view.js';
|
|
38
|
+
import { clearOpenConversation, renderConvList, refreshConversations, scheduleLoadedConversationsRefresh, applyConversationGroupEvent, openConversation } from './journal-view.js';
|
|
39
|
+
import { applyBackgroundTasksToConversation, applyTurnStatusToConversation } from './conversation-turn-state.mjs';
|
|
39
40
|
import { applyRelayEventSubscriptions, loadRelayEventSubscriptions } from './relay-event-subscriptions-ui.js';
|
|
40
41
|
import {
|
|
41
42
|
upsertRelayQuestion,
|
|
@@ -43,9 +44,13 @@ import {
|
|
|
43
44
|
updatePendingQuestionBanner,
|
|
44
45
|
} from './ask-user-view.js';
|
|
45
46
|
import { upsertRelayBoard, loadRelayBoards, renderRelayBoards } from './relay-board-view.js';
|
|
46
|
-
import { setConversationBackgroundTasks } from './background-tasks-view.mjs';
|
|
47
|
+
import { setConversationBackgroundTasks, setConversationTaskHealth } from './background-tasks-view.mjs';
|
|
48
|
+
import { handleTaskConsoleReconnect, handleTaskOutputChunk } from './task-console.mjs';
|
|
47
49
|
import { setConversationPins } from './pinned-messages-view.mjs';
|
|
50
|
+
import { setConversationHiddenMessages } from './hidden-messages-view.mjs';
|
|
48
51
|
import { setPreviews } from './preview-cards.mjs';
|
|
52
|
+
import { applyShareSettings, hasActiveShare, refreshShares } from './share-state.js';
|
|
53
|
+
import { handleShareOpened, handleViewerIpsForgotten } from './shared-conversations-settings.js';
|
|
49
54
|
import { setRemoteRelaysSnapshot } from './remote-relays-store.mjs';
|
|
50
55
|
import { REMOTE_RELAY_SOCKET_EVENT } from './remote-relay-shared.mjs';
|
|
51
56
|
import { AGENT_SESSIONS_SOCKET_EVENT } from './agent-sessions-settings.mjs';
|
|
@@ -74,10 +79,14 @@ import {
|
|
|
74
79
|
applyConversationTurnStatus,
|
|
75
80
|
renderMessages,
|
|
76
81
|
appendMessage,
|
|
82
|
+
captureLiveReplyReadingPosition,
|
|
83
|
+
restoreLiveReplyReadingPosition,
|
|
77
84
|
replaceRenderedMessage,
|
|
78
85
|
noteTranscriptLiveRow,
|
|
79
86
|
applyIncomingConversationDraftUpdate,
|
|
80
87
|
getRenderedConversationMessageFingerprints,
|
|
88
|
+
holdBackLiveRow,
|
|
89
|
+
isConversationWindowAtTail,
|
|
81
90
|
clearBubbleCancelState,
|
|
82
91
|
removeUserBubbleCancelButton,
|
|
83
92
|
updateSubagentBubbleFromStatus,
|
|
@@ -85,11 +94,23 @@ import {
|
|
|
85
94
|
syncComposerButtonState,
|
|
86
95
|
syncCancellableSteerButtons,
|
|
87
96
|
} from './conversation-view.js';
|
|
97
|
+
import { followsTranscriptEnd, noteTranscriptNews } from './transcript-jump-bottom-view.mjs';
|
|
88
98
|
import { loadRepoBrowserTree, refreshRepoBrowserIfWorkspaceOpen } from './attachments-view.js';
|
|
89
99
|
import { clearMessageSearchRuntimeState } from './message-search-view.js';
|
|
90
100
|
import { stripRelayPromptContext } from './relay-prompt-sanitizer.mjs';
|
|
91
101
|
import { isLikelyLiveDuplicateMessage } from './live-message-dedupe.mjs';
|
|
92
102
|
import { mergeRelayThoughts } from './relay-thoughts.mjs';
|
|
103
|
+
import { conversationPayloadCache } from './conversation-payload-cache.mjs';
|
|
104
|
+
|
|
105
|
+
// A live row of the conversation on screen joins its kept window, so a
|
|
106
|
+
// switch away and back still shows it. Rows of other conversations do not:
|
|
107
|
+
// their kept copy is what the reader saw, and the switch back fetches the rest.
|
|
108
|
+
function keepLiveRow(conversationId, message, messageId) {
|
|
109
|
+
if (IS_SHARED_VIEW || !message || typeof message !== 'object') return;
|
|
110
|
+
const id = String(message.id || messageId || '').trim();
|
|
111
|
+
if (!id) return;
|
|
112
|
+
conversationPayloadCache.upsertMessage(conversationId, message.id ? message : { ...message, id });
|
|
113
|
+
}
|
|
93
114
|
import {
|
|
94
115
|
handleSubagentStatusEventForViewer,
|
|
95
116
|
handleSubagentTimelineEvent,
|
|
@@ -329,16 +350,26 @@ export async function connectSocket(overrideDeps) {
|
|
|
329
350
|
// session, whose restored socket.data the server deliberately resets.
|
|
330
351
|
emitDeviceVisibility(document.visibilityState === 'visible');
|
|
331
352
|
clearMessageSearchRuntimeState();
|
|
353
|
+
// Events missed while the socket was down may have changed any of the
|
|
354
|
+
// conversations kept for a quick switch back.
|
|
355
|
+
conversationPayloadCache.clear();
|
|
332
356
|
setRelayOnline(true);
|
|
333
357
|
setCliOnline(true);
|
|
334
358
|
renderConvList();
|
|
335
359
|
resyncAfterConnect();
|
|
360
|
+
// Every loaded page again, not page one only: events missed while the
|
|
361
|
+
// socket was down touched rows anywhere in the list.
|
|
362
|
+
scheduleLoadedConversationsRefresh();
|
|
336
363
|
refreshSessionWorkerStatus().catch(() => {});
|
|
337
364
|
refreshModelCatalog().catch(() => {});
|
|
338
365
|
// An open sub-agent viewer fetches whatever it missed while away.
|
|
339
366
|
handleSubagentViewerReconnect();
|
|
367
|
+
// An open task console watches again and reads what it missed.
|
|
368
|
+
void handleTaskConsoleReconnect();
|
|
340
369
|
// Subscriptions that changed while away.
|
|
341
370
|
void loadRelayEventSubscriptions();
|
|
371
|
+
// Shares made or stopped while away.
|
|
372
|
+
void refreshShares().catch(() => {});
|
|
342
373
|
});
|
|
343
374
|
socket.on('connect_error', (e) => {
|
|
344
375
|
setRelayOnline(false);
|
|
@@ -508,16 +539,23 @@ export async function connectSocket(overrideDeps) {
|
|
|
508
539
|
})) {
|
|
509
540
|
return;
|
|
510
541
|
}
|
|
542
|
+
// A window that ends above the conversation's end takes no live row:
|
|
543
|
+
// the newer pages bring it, and the jump-to-bottom badge counts it.
|
|
544
|
+
if (holdBackLiveRow(conversationId, messageId || normalizedMessage.id)) return;
|
|
511
545
|
// Another client's message (an agent's prompt included) follows the
|
|
512
546
|
// end like any live row: a reader scrolled up stays where they are,
|
|
513
547
|
// and the jump-to-bottom badge counts it.
|
|
514
|
-
appendMessage(normalizedMessage,
|
|
548
|
+
appendMessage(normalizedMessage, followsTranscriptEnd(), messageId);
|
|
515
549
|
noteTranscriptLiveRow();
|
|
550
|
+
keepLiveRow(conversationId, normalizedMessage, messageId);
|
|
516
551
|
}
|
|
517
552
|
});
|
|
518
553
|
socket.on('assistant_message', ({ conversationId, message, messageId, sourceMessageId }) => {
|
|
519
554
|
const isCurrentConversation = conversationId === currentConvId;
|
|
520
|
-
const autoScroll = isCurrentConversation ?
|
|
555
|
+
const autoScroll = isCurrentConversation ? followsTranscriptEnd() : false;
|
|
556
|
+
// A reader who is not at the end keeps what they read where it is while
|
|
557
|
+
// the reply takes the live bubble's place.
|
|
558
|
+
const reading = isCurrentConversation && !autoScroll ? captureLiveReplyReadingPosition() : null;
|
|
521
559
|
// A background conversation finishing its turn must not wipe the viewed
|
|
522
560
|
// conversation's live bubble.
|
|
523
561
|
if (isCurrentConversation) {
|
|
@@ -536,12 +574,19 @@ export async function connectSocket(overrideDeps) {
|
|
|
536
574
|
message.thoughts = mergedThoughts;
|
|
537
575
|
}
|
|
538
576
|
}
|
|
539
|
-
if (messageId && seenMessageIds?.has(messageId))
|
|
540
|
-
|
|
577
|
+
if (messageId && seenMessageIds?.has(messageId)) {
|
|
578
|
+
restoreLiveReplyReadingPosition(reading);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
// Held back under a window that ends above the conversation's end, as
|
|
582
|
+
// in user_message.
|
|
583
|
+
if (isCurrentConversation && !holdBackLiveRow(conversationId, messageId || message?.id)) {
|
|
541
584
|
appendMessage(message, autoScroll, messageId || null, false, sourceMessageId || null);
|
|
585
|
+
restoreLiveReplyReadingPosition(reading);
|
|
542
586
|
noteTranscriptLiveRow();
|
|
543
|
-
|
|
587
|
+
keepLiveRow(conversationId, message, messageId);
|
|
544
588
|
}
|
|
589
|
+
if (isCurrentConversation) scheduleContextUsageRefresh(conversationId, 120);
|
|
545
590
|
if (sourceMessageId) relayActivities.delete(sourceMessageId);
|
|
546
591
|
if (sourceMessageId) relayThoughts.delete(sourceMessageId);
|
|
547
592
|
if (sourceMessageId) clearRelayStreamStateForMessage(sourceMessageId);
|
|
@@ -556,11 +601,22 @@ export async function connectSocket(overrideDeps) {
|
|
|
556
601
|
// cannot be started): same id, new text or kind. Not on the page yet (the
|
|
557
602
|
// conversation was opened after it was written): appended like any other.
|
|
558
603
|
socket.on('message_updated', ({ conversationId, messageId, message }) => {
|
|
559
|
-
|
|
604
|
+
// A row rewritten in place. Another conversation's kept window would show
|
|
605
|
+
// the old one: it is dropped. The window on screen takes the new row as
|
|
606
|
+
// the page does, so a switch away and back keeps both the row and the
|
|
607
|
+
// window around it.
|
|
608
|
+
if (!messageId || conversationId !== currentConvId || !message) {
|
|
609
|
+
conversationPayloadCache.forget(conversationId);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
560
612
|
if (!replaceRenderedMessage(message, messageId)) {
|
|
561
|
-
|
|
613
|
+
// A row outside a window that ends above the conversation's end: the
|
|
614
|
+
// newer pages bring it as it is now.
|
|
615
|
+
if (!isConversationWindowAtTail()) return;
|
|
616
|
+
appendMessage(message, followsTranscriptEnd(), messageId, false);
|
|
562
617
|
}
|
|
563
618
|
noteTranscriptLiveRow();
|
|
619
|
+
keepLiveRow(conversationId, message, messageId);
|
|
564
620
|
});
|
|
565
621
|
socket.on('relay_question', ({ question }) => upsertRelayQuestion(question));
|
|
566
622
|
socket.on('relay_question_updated', ({ question }) => upsertRelayQuestion(question));
|
|
@@ -576,19 +632,61 @@ export async function connectSocket(overrideDeps) {
|
|
|
576
632
|
socket.on('relay_event_subscriptions', ({ subscriptions }) => {
|
|
577
633
|
applyRelayEventSubscriptions(subscriptions);
|
|
578
634
|
});
|
|
579
|
-
socket.on('background_tasks', ({ conversationId, tasks }) => {
|
|
580
|
-
setConversationBackgroundTasks(conversationId, tasks);
|
|
635
|
+
socket.on('background_tasks', ({ conversationId, tasks, endedTasks }) => {
|
|
636
|
+
setConversationBackgroundTasks(conversationId, tasks, { endedTasks });
|
|
637
|
+
// The sidebar's row (and its group's head) count background work as
|
|
638
|
+
// running; the set arrives under the conversation or its session id.
|
|
639
|
+
const id = String(conversationId || '').trim();
|
|
640
|
+
const record = conversations[id]
|
|
641
|
+
|| Object.values(conversations).find((conversation) => String(conversation?.sdkSessionId || '') === id)
|
|
642
|
+
|| null;
|
|
643
|
+
if (record && applyBackgroundTasksToConversation(record, tasks)) renderConvList();
|
|
644
|
+
});
|
|
645
|
+
// The relay's health poller: last output time and test-runner counts of
|
|
646
|
+
// shell and Monitor tasks, only when they changed.
|
|
647
|
+
socket.on('background_tasks_health', ({ conversationId, health }) => {
|
|
648
|
+
setConversationTaskHealth(conversationId, health);
|
|
649
|
+
});
|
|
650
|
+
// New bytes of the task the console shows; the relay sends them only while
|
|
651
|
+
// this page's console watches that task.
|
|
652
|
+
socket.on('task_output_chunk', (payload) => {
|
|
653
|
+
handleTaskOutputChunk(payload);
|
|
581
654
|
});
|
|
582
655
|
// A message was pinned or unpinned (any client): the conversation's whole
|
|
583
656
|
// pin list, replacing what this page had.
|
|
584
657
|
socket.on('conversation_pins_updated', ({ conversationId, pins, revision }) => {
|
|
585
658
|
setConversationPins(conversationId, pins, revision);
|
|
586
659
|
});
|
|
660
|
+
// A message was hidden from or shown to shared viewers (any client): the
|
|
661
|
+
// conversation's whole hidden list, replacing what this page had.
|
|
662
|
+
socket.on('conversation_hidden_messages_updated', ({ conversationId, hiddenMessages, revision }) => {
|
|
663
|
+
setConversationHiddenMessages(conversationId, hiddenMessages, revision);
|
|
664
|
+
});
|
|
587
665
|
// REPLACE semantics for the whole set: the preview registry is relay-owned
|
|
588
666
|
// and global, so every change ships the full list.
|
|
589
667
|
socket.on('previews', ({ previews }) => {
|
|
590
668
|
setPreviews(previews);
|
|
591
669
|
});
|
|
670
|
+
// Public shares: the events name the conversation only; the list (Settings
|
|
671
|
+
// → Sharing, the ⋯ menu's Stop sharing) is read again.
|
|
672
|
+
for (const event of ['share_created', 'share_renewed', 'share_revoked']) {
|
|
673
|
+
socket.on(event, () => {
|
|
674
|
+
void refreshShares().catch(() => {});
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
// A deleted conversation takes its share along.
|
|
678
|
+
socket.on('conversation_deleted', ({ conversationId } = {}) => {
|
|
679
|
+
if (hasActiveShare(conversationId)) void refreshShares().catch(() => {});
|
|
680
|
+
});
|
|
681
|
+
socket.on('share_opened', ({ conversationId } = {}) => {
|
|
682
|
+
handleShareOpened(conversationId);
|
|
683
|
+
});
|
|
684
|
+
socket.on('share_settings_changed', (payload) => {
|
|
685
|
+
applyShareSettings(payload || null);
|
|
686
|
+
});
|
|
687
|
+
socket.on('share_viewer_ips_forgotten', () => {
|
|
688
|
+
handleViewerIpsForgotten();
|
|
689
|
+
});
|
|
592
690
|
// REPLACE semantics too: the relay ships the whole remote-relay list (no
|
|
593
691
|
// tokens) whenever a remote is added, removed, renamed or changes status.
|
|
594
692
|
socket.on(REMOTE_RELAY_SOCKET_EVENT, (payload) => {
|
|
@@ -613,16 +711,17 @@ export async function connectSocket(overrideDeps) {
|
|
|
613
711
|
const last = items[items.length - 1];
|
|
614
712
|
const lastText = typeof last === 'string' ? last : String(last?.text || '');
|
|
615
713
|
const lastSubagentRunId = typeof last === 'object' && last ? (last.subagentRunId || null) : null;
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
714
|
+
// The same line again (a repeated event) adds nothing.
|
|
715
|
+
const added = lastText !== entry.text || lastSubagentRunId !== entry.subagentRunId;
|
|
716
|
+
if (added) relayActivities.set(messageId, capLiveActivityEntries(items.concat(entry)));
|
|
619
717
|
if (entry.subagentRunId) {
|
|
620
718
|
upsertSubagentRun({ subagentRunId: entry.subagentRunId, messageId, conversationId });
|
|
621
719
|
addSubagentActivity(entry.subagentRunId, entry.text);
|
|
622
720
|
}
|
|
623
721
|
if (conversationId === currentConvId) {
|
|
624
|
-
const autoScroll =
|
|
722
|
+
const autoScroll = followsTranscriptEnd();
|
|
625
723
|
appendThinkingActivity(entry, entry.subagentRunId, autoScroll);
|
|
724
|
+
if (added && !autoScroll) noteTranscriptNews();
|
|
626
725
|
}
|
|
627
726
|
});
|
|
628
727
|
socket.on('relay_stream', ({ conversationId, messageId, text, done, seq, subagentRunId }) => {
|
|
@@ -631,8 +730,8 @@ export async function connectSocket(overrideDeps) {
|
|
|
631
730
|
upsertSubagentRun({ subagentRunId: String(subagentRunId).trim(), messageId, conversationId });
|
|
632
731
|
}
|
|
633
732
|
if (conversationId !== currentConvId) return;
|
|
634
|
-
const autoScroll =
|
|
635
|
-
applyRelayStreamEvent({
|
|
733
|
+
const autoScroll = followsTranscriptEnd();
|
|
734
|
+
const accepted = applyRelayStreamEvent({
|
|
636
735
|
messageId,
|
|
637
736
|
text: String(text || ''),
|
|
638
737
|
done: !!done,
|
|
@@ -640,11 +739,16 @@ export async function connectSocket(overrideDeps) {
|
|
|
640
739
|
subagentRunId: subagentRunId ? String(subagentRunId).trim() : null,
|
|
641
740
|
autoScroll,
|
|
642
741
|
});
|
|
742
|
+
// A frame that was dropped (out of order, a finished turn) is no news.
|
|
743
|
+
if (accepted && !autoScroll) noteTranscriptNews();
|
|
643
744
|
});
|
|
644
745
|
socket.on('relay_thought', ({ conversationId, messageId, reasoningId, text, done, subagentRunId }) => {
|
|
645
746
|
if (!messageId) return;
|
|
646
747
|
const key = String(reasoningId || 'reasoning');
|
|
647
748
|
const thoughtMap = relayThoughts.get(messageId) || new Map();
|
|
749
|
+
const known = thoughtMap.get(key);
|
|
750
|
+
// The same thought again (a repeated event) adds nothing.
|
|
751
|
+
const added = !known || known.text !== String(text || '') || known.done !== !!done;
|
|
648
752
|
thoughtMap.set(key, { reasoningId: key, text: String(text || ''), done: !!done, subagentRunId: subagentRunId || null });
|
|
649
753
|
relayThoughts.set(messageId, thoughtMap);
|
|
650
754
|
if (subagentRunId) {
|
|
@@ -652,8 +756,9 @@ export async function connectSocket(overrideDeps) {
|
|
|
652
756
|
addSubagentThought(subagentRunId, { reasoningId: key, text: String(text || ''), done: !!done });
|
|
653
757
|
}
|
|
654
758
|
if (conversationId === currentConvId) {
|
|
655
|
-
const autoScroll =
|
|
759
|
+
const autoScroll = followsTranscriptEnd();
|
|
656
760
|
appendThinkingThought(key, String(text || ''), !!done, subagentRunId, autoScroll);
|
|
761
|
+
if (added && !autoScroll) noteTranscriptNews();
|
|
657
762
|
}
|
|
658
763
|
});
|
|
659
764
|
socket.on('subagent_status', (payload = {}) => {
|
|
@@ -681,6 +786,8 @@ export async function connectSocket(overrideDeps) {
|
|
|
681
786
|
});
|
|
682
787
|
socket.on('conversation_compacted', async ({ sourceConversationId, targetConversationId }) => {
|
|
683
788
|
if (!sourceConversationId || !targetConversationId) return;
|
|
789
|
+
conversationPayloadCache.forget(sourceConversationId);
|
|
790
|
+
conversationPayloadCache.forget(targetConversationId);
|
|
684
791
|
await refreshConversations();
|
|
685
792
|
if (currentConvId === sourceConversationId) {
|
|
686
793
|
await openConversation(targetConversationId);
|
|
@@ -770,6 +877,9 @@ export async function connectSocket(overrideDeps) {
|
|
|
770
877
|
applyConversationTurnStatus({ conversationId, messageId, status });
|
|
771
878
|
if (conversationId && conversations[conversationId]) {
|
|
772
879
|
const conversation = conversations[conversationId];
|
|
880
|
+
// The row's turn fields follow the event, open or not: the title colour, a
|
|
881
|
+
// group head's pill and its fold read them between list fetches.
|
|
882
|
+
applyTurnStatusToConversation(conversation, normalizedStatus);
|
|
773
883
|
if (normalizedStatus === 'processing') {
|
|
774
884
|
conversation.localTurnStatus = 'processing';
|
|
775
885
|
conversation.localTurnStatusUpdatedAt = Date.now();
|
|
@@ -785,7 +895,7 @@ export async function connectSocket(overrideDeps) {
|
|
|
785
895
|
}
|
|
786
896
|
}
|
|
787
897
|
if (conversationId === currentConvId && normalizedStatus === 'processing') {
|
|
788
|
-
const autoScroll =
|
|
898
|
+
const autoScroll = followsTranscriptEnd();
|
|
789
899
|
showThinking(messageId || null, autoScroll);
|
|
790
900
|
renderThinkingActivities();
|
|
791
901
|
renderThinkingThoughts();
|
|
@@ -821,11 +931,18 @@ export async function connectSocket(overrideDeps) {
|
|
|
821
931
|
}
|
|
822
932
|
renderConvList();
|
|
823
933
|
});
|
|
934
|
+
// A session group changed (a member joined or left, a name, a dissolve,
|
|
935
|
+
// here or from another device): every loaded page is fetched again with
|
|
936
|
+
// its groups, so a row far down the list changes too.
|
|
937
|
+
socket.on('conversation_group_changed', (payload) => {
|
|
938
|
+
applyConversationGroupEvent(payload);
|
|
939
|
+
});
|
|
824
940
|
socket.on('conversation_archived', ({ conversationId, archived }) => {
|
|
825
941
|
const id = String(conversationId || '').trim();
|
|
826
942
|
if (!id) return;
|
|
827
943
|
const conversation = conversations[id];
|
|
828
944
|
if (conversation) conversation.archived = archived !== false;
|
|
945
|
+
conversationPayloadCache.forget(id);
|
|
829
946
|
renderConvList();
|
|
830
947
|
if (currentConvId === id) {
|
|
831
948
|
syncChatTitleControls();
|
|
@@ -834,6 +951,7 @@ export async function connectSocket(overrideDeps) {
|
|
|
834
951
|
});
|
|
835
952
|
socket.on('conversation_deleted', ({ conversationId }) => {
|
|
836
953
|
delete conversations[conversationId];
|
|
954
|
+
conversationPayloadCache.forget(conversationId);
|
|
837
955
|
for (const [id, question] of relayQuestions.entries()) {
|
|
838
956
|
if (question?.conversationId === conversationId) relayQuestions.delete(id);
|
|
839
957
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { resolveNearBottomThresholdPx } from './transcript-viewport-anchor.mjs';
|
|
2
|
+
import { normalizeReadingAnchor } from './transcript-reading-anchor.mjs';
|
|
2
3
|
import { recordCliLifecycleEvent, recordRelayLifecycleEvent } from './status-store.mjs';
|
|
3
4
|
import { resolveRelayDotState } from './relay-dot-state.mjs';
|
|
4
5
|
import { readRepoBrowserPreferences } from './repo-browser-preferences.mjs';
|
|
5
6
|
import { isChatInteractionHeld } from './selection-guard.mjs';
|
|
6
7
|
import { nextSubagentStatus } from './subagent-fold.mjs';
|
|
7
8
|
import { useStrictStrikethrough } from './markdown-strikethrough.mjs';
|
|
9
|
+
import { parseGroupFoldChoices, pruneGroupFoldChoices } from './conversation-group-state.mjs';
|
|
8
10
|
|
|
9
11
|
function resolveAppBase() {
|
|
10
12
|
const configuredBase = typeof window.__COPILOT_APP_CONFIG?.basePath === 'string'
|
|
@@ -16,6 +18,8 @@ function resolveAppBase() {
|
|
|
16
18
|
const pathname = String(window.location.pathname || '/');
|
|
17
19
|
const sharedIndex = pathname.indexOf('/shared/');
|
|
18
20
|
if (sharedIndex >= 0) return pathname.slice(0, sharedIndex).replace(/\/+$/, '');
|
|
21
|
+
const previewIndex = pathname.indexOf('/share-preview/');
|
|
22
|
+
if (previewIndex >= 0) return pathname.slice(0, previewIndex).replace(/\/+$/, '');
|
|
19
23
|
return pathname.replace(/\/+$/, '');
|
|
20
24
|
}
|
|
21
25
|
|
|
@@ -32,8 +36,24 @@ function resolveSharedConversationToken() {
|
|
|
32
36
|
if (configured) return configured;
|
|
33
37
|
return extractSharedTokenFromPathname(window.location.pathname);
|
|
34
38
|
}
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
// "Preview as shared": the owner reads a conversation as a share link would
|
|
40
|
+
// serve it (marked redactions), through the owner-only preview routes. It is a
|
|
41
|
+
// shared view in every other respect.
|
|
42
|
+
function resolveSharePreviewConversationId() {
|
|
43
|
+
const configured = String(window.__COPILOT_APP_CONFIG?.sharePreviewConversationId || '').trim();
|
|
44
|
+
if (configured) return configured;
|
|
45
|
+
const match = String(window.location.pathname || '/').match(/\/share-preview\/([^/?#]+)\/?$/i);
|
|
46
|
+
if (!match) return '';
|
|
47
|
+
try {
|
|
48
|
+
return decodeURIComponent(String(match[1] || '')).trim();
|
|
49
|
+
} catch {
|
|
50
|
+
return '';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export const SHARE_PREVIEW_CONVERSATION_ID = resolveSharePreviewConversationId();
|
|
54
|
+
export const IS_SHARE_PREVIEW = !!SHARE_PREVIEW_CONVERSATION_ID;
|
|
55
|
+
export const SHARED_CONVERSATION_TOKEN = IS_SHARE_PREVIEW ? '' : resolveSharedConversationToken();
|
|
56
|
+
export const IS_SHARED_VIEW = !!SHARED_CONVERSATION_TOKEN || IS_SHARE_PREVIEW;
|
|
37
57
|
export let TOKEN = '';
|
|
38
58
|
export let socket = null;
|
|
39
59
|
export let currentConvId = null;
|
|
@@ -82,6 +102,9 @@ export let updateState = null;
|
|
|
82
102
|
export let activeRuntimeSessionCount = 0;
|
|
83
103
|
export let runtimeSessionBindingCount = 0;
|
|
84
104
|
export let conversations = {};
|
|
105
|
+
// Session groups by id, as the list payload describes them (`groups`):
|
|
106
|
+
// name, display name, head and member counts. Merged with every page.
|
|
107
|
+
export const conversationGroups = {};
|
|
85
108
|
export let selectedAttachments = [];
|
|
86
109
|
export let imageEditTarget = null;
|
|
87
110
|
export const RELAY_QUESTION_POLL_MS = 3000;
|
|
@@ -172,6 +195,8 @@ const SIDEBAR_WIDTH_PERCENT_FALLBACK = 24;
|
|
|
172
195
|
const SIDEBAR_WIDTH_PORTRAIT_STORAGE_KEY = 'copilot_sidebar_width_pct_portrait';
|
|
173
196
|
const SIDEBAR_WIDTH_LANDSCAPE_STORAGE_KEY = 'copilot_sidebar_width_pct_landscape';
|
|
174
197
|
const SIDEBAR_COLLAPSED_DESKTOP_STORAGE_KEY = 'copilot_sidebar_collapsed_desktop';
|
|
198
|
+
// The user's fold choices for session groups in the sidebar: { [groupId]: 'open' | 'folded' }.
|
|
199
|
+
const CONVERSATION_GROUP_FOLDS_STORAGE_KEY = 'copilot_conv_group_folds';
|
|
175
200
|
let sidebarWidthPercent = SIDEBAR_WIDTH_PERCENT_FALLBACK;
|
|
176
201
|
const CONVERSATION_SCROLL_STORAGE_PREFIX = 'copilot_message_scroll_';
|
|
177
202
|
|
|
@@ -197,6 +222,59 @@ export function setCurrentConv(id) {
|
|
|
197
222
|
updateCompactButton();
|
|
198
223
|
}
|
|
199
224
|
|
|
225
|
+
/** Merges a list payload's `groups` (by id) into the known session groups. */
|
|
226
|
+
export function mergeConversationGroups(groups) {
|
|
227
|
+
if (!groups || typeof groups !== 'object') return;
|
|
228
|
+
for (const [groupId, group] of Object.entries(groups)) {
|
|
229
|
+
const id = String(groupId || '').trim();
|
|
230
|
+
if (!id || !group || typeof group !== 'object') continue;
|
|
231
|
+
conversationGroups[id] = { ...(conversationGroups[id] || {}), ...group, id };
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Forgets a session group the relay no longer lists. */
|
|
236
|
+
export function forgetConversationGroup(groupId) {
|
|
237
|
+
delete conversationGroups[String(groupId || '').trim()];
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Keeps only the groups that a loaded row still belongs to: after a list
|
|
242
|
+
* refresh a dissolved group has no row left, and the picker must not offer
|
|
243
|
+
* it. Returns the ids dropped.
|
|
244
|
+
*/
|
|
245
|
+
export function pruneConversationGroups(keepGroupIds) {
|
|
246
|
+
const keep = new Set((Array.isArray(keepGroupIds) ? keepGroupIds : []).map((id) => String(id || '').trim()).filter(Boolean));
|
|
247
|
+
const dropped = [];
|
|
248
|
+
for (const id of Object.keys(conversationGroups)) {
|
|
249
|
+
if (keep.has(id)) continue;
|
|
250
|
+
delete conversationGroups[id];
|
|
251
|
+
dropped.push(id);
|
|
252
|
+
}
|
|
253
|
+
return dropped;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function readConversationGroupFoldChoices() {
|
|
257
|
+
return parseGroupFoldChoices(localStorage.getItem(CONVERSATION_GROUP_FOLDS_STORAGE_KEY) || '');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// The stored fold choices are trimmed to the loaded groups only past this
|
|
261
|
+
// size: a choice for a group on an older page or in the other view must
|
|
262
|
+
// not go with every toggle.
|
|
263
|
+
const CONVERSATION_GROUP_FOLDS_MAX = 300;
|
|
264
|
+
|
|
265
|
+
/** Stores the user's fold choice for a group. */
|
|
266
|
+
export function setConversationGroupFoldChoice(groupId, choice) {
|
|
267
|
+
const id = String(groupId || '').trim();
|
|
268
|
+
if (!id) return;
|
|
269
|
+
let next = readConversationGroupFoldChoices();
|
|
270
|
+
if (Object.keys(next).length > CONVERSATION_GROUP_FOLDS_MAX) {
|
|
271
|
+
next = pruneGroupFoldChoices(next, [...Object.keys(conversationGroups), id]);
|
|
272
|
+
}
|
|
273
|
+
if (choice === 'open' || choice === 'folded') next[id] = choice;
|
|
274
|
+
else delete next[id];
|
|
275
|
+
localStorage.setItem(CONVERSATION_GROUP_FOLDS_STORAGE_KEY, JSON.stringify(next));
|
|
276
|
+
}
|
|
277
|
+
|
|
200
278
|
export function setConversationWatcherCount(conversationId, count) {
|
|
201
279
|
const id = String(conversationId || '').trim();
|
|
202
280
|
if (!id) return;
|
|
@@ -236,6 +314,7 @@ function readConversationViewState(conversationId) {
|
|
|
236
314
|
loadedMessageCount: Number.isFinite(Number(parsed.loadedMessageCount)) && Number(parsed.loadedMessageCount) >= 0
|
|
237
315
|
? Math.trunc(Number(parsed.loadedMessageCount))
|
|
238
316
|
: null,
|
|
317
|
+
anchor: normalizeReadingAnchor(parsed.anchor),
|
|
239
318
|
};
|
|
240
319
|
}
|
|
241
320
|
} catch {
|
|
@@ -246,6 +325,7 @@ function readConversationViewState(conversationId) {
|
|
|
246
325
|
return {
|
|
247
326
|
scrollTop: Math.trunc(legacyScrollTop),
|
|
248
327
|
loadedMessageCount: null,
|
|
328
|
+
anchor: null,
|
|
249
329
|
};
|
|
250
330
|
}
|
|
251
331
|
|
|
@@ -260,6 +340,7 @@ function writeConversationViewState(conversationId, nextState = {}) {
|
|
|
260
340
|
loadedMessageCount: Number.isFinite(Number(nextState.loadedMessageCount)) && Number(nextState.loadedMessageCount) >= 0
|
|
261
341
|
? Math.trunc(Number(nextState.loadedMessageCount))
|
|
262
342
|
: current.loadedMessageCount ?? null,
|
|
343
|
+
anchor: 'anchor' in nextState ? normalizeReadingAnchor(nextState.anchor) : (current.anchor ?? null),
|
|
263
344
|
};
|
|
264
345
|
localStorage.setItem(key, JSON.stringify(next));
|
|
265
346
|
return true;
|
|
@@ -296,6 +377,27 @@ export function loadConversationLoadedMessageCount(conversationId) {
|
|
|
296
377
|
return readConversationViewState(conversationId)?.loadedMessageCount ?? null;
|
|
297
378
|
}
|
|
298
379
|
|
|
380
|
+
// The message read and its offset, or "at the end" (transcript-reading-anchor.mjs):
|
|
381
|
+
// what a switch back and a reload restore. scrollTop is still written for
|
|
382
|
+
// older pages of this app reading the same storage.
|
|
383
|
+
export function saveConversationReadingAnchor(conversationId, anchor) {
|
|
384
|
+
const value = normalizeReadingAnchor(anchor);
|
|
385
|
+
if (!value) return false;
|
|
386
|
+
try {
|
|
387
|
+
return writeConversationViewState(conversationId, { anchor: value });
|
|
388
|
+
} catch {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function loadConversationReadingAnchor(conversationId) {
|
|
394
|
+
try {
|
|
395
|
+
return readConversationViewState(conversationId)?.anchor ?? null;
|
|
396
|
+
} catch {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
299
401
|
function normalizePendingMessageText(text) {
|
|
300
402
|
return String(text || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
301
403
|
}
|
|
@@ -612,37 +714,18 @@ export function getMessagesDistanceFromBottom() {
|
|
|
612
714
|
return Math.max(0, el.scrollHeight - el.clientHeight - el.scrollTop);
|
|
613
715
|
}
|
|
614
716
|
|
|
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) {
|
|
717
|
+
/**
|
|
718
|
+
* "At the end" of the transcript, wherever that is decided: within the
|
|
719
|
+
* near-bottom margin (8% of the viewport, between 12 and 48 px). One margin
|
|
720
|
+
* and no parameter: a caller with a margin of its own (there was one with
|
|
721
|
+
* 80 px, one with 1 px, and a default that read as 0 px) breaks the rule that
|
|
722
|
+
* the transcript follows a reader who is at the end and nobody else.
|
|
723
|
+
*/
|
|
724
|
+
export function isMessagesNearBottom() {
|
|
631
725
|
const el = document.getElementById('messages');
|
|
632
726
|
if (!el) return true;
|
|
633
|
-
const epsilon = Math.max(0, Number.isFinite(Number(epsilonPx)) ? Number(epsilonPx) : 1);
|
|
634
727
|
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);
|
|
728
|
+
return distance <= resolveNearBottomThresholdPx(el.clientHeight);
|
|
646
729
|
}
|
|
647
730
|
|
|
648
731
|
export function isMobileComposerViewport() {
|
|
@@ -944,8 +1027,16 @@ function buildSessionWorkerStateHash(map) {
|
|
|
944
1027
|
|
|
945
1028
|
let sessionWorkerStateHash = '';
|
|
946
1029
|
|
|
1030
|
+
// Whether a status poll has answered yet: before that, a session's reported
|
|
1031
|
+
// background work is taken as it is; after it, only a live worker carries it.
|
|
1032
|
+
let sessionWorkerStatusKnown = false;
|
|
1033
|
+
export function hasSessionWorkerStatus() {
|
|
1034
|
+
return sessionWorkerStatusKnown;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
947
1037
|
export function setSessionWorkerStatesFromStatusPayload(payload) {
|
|
948
1038
|
const workers = Array.isArray(payload?.workers) ? payload.workers : [];
|
|
1039
|
+
sessionWorkerStatusKnown = true;
|
|
949
1040
|
const next = new Map();
|
|
950
1041
|
for (const worker of workers) {
|
|
951
1042
|
const normalized = normalizeWorkerStateEntry(worker);
|
|
@@ -60,9 +60,9 @@ export function isRunningSubagentStatus(status) {
|
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* The row for one run. `clickable` makes the title the button that opens the
|
|
63
|
-
* viewer (`data-subagent-open`); a shared view
|
|
64
|
-
*
|
|
65
|
-
* size the row differently.
|
|
63
|
+
* viewer (`data-subagent-open`); a shared view opens it too, read-only,
|
|
64
|
+
* through the share's sub-agent route. `context` lands in a class so the
|
|
65
|
+
* bubble and the viewer can size the row differently.
|
|
66
66
|
*/
|
|
67
67
|
export function subagentRowHtml(run, { context = 'bubble', clickable = true, now = Date.now() } = {}) {
|
|
68
68
|
const runId = String(run?.subagentRunId || '').trim();
|