@oar-sh/oar 0.9.10 → 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 +314 -1
- package/README.md +156 -972
- package/package.json +13 -13
- package/server/README.md +95 -22
- package/server/claude-worker/claude-session-process.mjs +89 -11
- package/server/claude-worker/claude-session-worker.mjs +9 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +76 -11
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +6 -0
- package/server/db-schema.mjs +59 -2
- package/server/grok-worker/acp-client.mjs +2 -1
- package/server/grok-worker/acp-host-services.mjs +4 -3
- 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 +40 -101
- package/server/public/app/api-client.js +251 -16
- package/server/public/app/ask-user-view.js +17 -15
- package/server/public/app/bootstrap.js +158 -32
- package/server/public/app/chat-actions-menu-state.mjs +20 -10
- 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 +136 -93
- 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 +787 -22
- 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-restart-error.mjs +16 -0
- package/server/public/app/settings-modal.js +40 -8
- 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 +80 -14
- package/server/public/app/store.js +96 -30
- package/server/public/app/subagent-row.mjs +3 -3
- package/server/public/app/subagent-viewer.mjs +30 -6
- package/server/public/app/thinking-follow.js +0 -4
- package/server/public/app/transcript-jump-bottom-view.mjs +194 -20
- package/server/public/app/transcript-jump-bottom.mjs +132 -21
- package/server/public/app/transcript-viewport-anchor.mjs +140 -0
- package/server/public/index.html +386 -15
- 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 +1 -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 +184 -65
- package/server/routes/preview-routes.mjs +9 -1
- package/server/routes/sessions-routes.mjs +771 -193
- package/server/server-runtime.mjs +316 -100
- 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/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/remote-relay-dispatcher.mjs +57 -5
- package/server/services/request-boundary.mjs +183 -0
- package/server/services/session-activity-tracker.mjs +227 -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/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/preview-tool-core.mjs +15 -5
- package/shared/remote-relay-contract.mjs +3 -1
- package/shared/share-path-privacy.mjs +30 -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
|
@@ -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,
|
|
@@ -35,7 +34,8 @@ import {
|
|
|
35
34
|
import { scheduleContextUsageRefresh } from './api-client.js';
|
|
36
35
|
import { capLiveActivityEntries } from './activity-replay-state.mjs';
|
|
37
36
|
import { publishStatusEvent, recordStatusEvent } from './status-store.mjs';
|
|
38
|
-
import { clearOpenConversation, 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
39
|
import { applyRelayEventSubscriptions, loadRelayEventSubscriptions } from './relay-event-subscriptions-ui.js';
|
|
40
40
|
import {
|
|
41
41
|
upsertRelayQuestion,
|
|
@@ -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';
|
|
@@ -74,6 +77,8 @@ import {
|
|
|
74
77
|
applyConversationTurnStatus,
|
|
75
78
|
renderMessages,
|
|
76
79
|
appendMessage,
|
|
80
|
+
captureLiveReplyReadingPosition,
|
|
81
|
+
restoreLiveReplyReadingPosition,
|
|
77
82
|
replaceRenderedMessage,
|
|
78
83
|
noteTranscriptLiveRow,
|
|
79
84
|
applyIncomingConversationDraftUpdate,
|
|
@@ -85,6 +90,7 @@ import {
|
|
|
85
90
|
syncComposerButtonState,
|
|
86
91
|
syncCancellableSteerButtons,
|
|
87
92
|
} from './conversation-view.js';
|
|
93
|
+
import { followsTranscriptEnd, noteTranscriptNews } from './transcript-jump-bottom-view.mjs';
|
|
88
94
|
import { loadRepoBrowserTree, refreshRepoBrowserIfWorkspaceOpen } from './attachments-view.js';
|
|
89
95
|
import { clearMessageSearchRuntimeState } from './message-search-view.js';
|
|
90
96
|
import { stripRelayPromptContext } from './relay-prompt-sanitizer.mjs';
|
|
@@ -333,12 +339,17 @@ export async function connectSocket(overrideDeps) {
|
|
|
333
339
|
setCliOnline(true);
|
|
334
340
|
renderConvList();
|
|
335
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();
|
|
336
345
|
refreshSessionWorkerStatus().catch(() => {});
|
|
337
346
|
refreshModelCatalog().catch(() => {});
|
|
338
347
|
// An open sub-agent viewer fetches whatever it missed while away.
|
|
339
348
|
handleSubagentViewerReconnect();
|
|
340
349
|
// Subscriptions that changed while away.
|
|
341
350
|
void loadRelayEventSubscriptions();
|
|
351
|
+
// Shares made or stopped while away.
|
|
352
|
+
void refreshShares().catch(() => {});
|
|
342
353
|
});
|
|
343
354
|
socket.on('connect_error', (e) => {
|
|
344
355
|
setRelayOnline(false);
|
|
@@ -511,13 +522,16 @@ export async function connectSocket(overrideDeps) {
|
|
|
511
522
|
// Another client's message (an agent's prompt included) follows the
|
|
512
523
|
// end like any live row: a reader scrolled up stays where they are,
|
|
513
524
|
// and the jump-to-bottom badge counts it.
|
|
514
|
-
appendMessage(normalizedMessage,
|
|
525
|
+
appendMessage(normalizedMessage, followsTranscriptEnd(), messageId);
|
|
515
526
|
noteTranscriptLiveRow();
|
|
516
527
|
}
|
|
517
528
|
});
|
|
518
529
|
socket.on('assistant_message', ({ conversationId, message, messageId, sourceMessageId }) => {
|
|
519
530
|
const isCurrentConversation = conversationId === currentConvId;
|
|
520
|
-
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;
|
|
521
535
|
// A background conversation finishing its turn must not wipe the viewed
|
|
522
536
|
// conversation's live bubble.
|
|
523
537
|
if (isCurrentConversation) {
|
|
@@ -536,9 +550,13 @@ export async function connectSocket(overrideDeps) {
|
|
|
536
550
|
message.thoughts = mergedThoughts;
|
|
537
551
|
}
|
|
538
552
|
}
|
|
539
|
-
if (messageId && seenMessageIds?.has(messageId))
|
|
553
|
+
if (messageId && seenMessageIds?.has(messageId)) {
|
|
554
|
+
restoreLiveReplyReadingPosition(reading);
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
540
557
|
if (isCurrentConversation) {
|
|
541
558
|
appendMessage(message, autoScroll, messageId || null, false, sourceMessageId || null);
|
|
559
|
+
restoreLiveReplyReadingPosition(reading);
|
|
542
560
|
noteTranscriptLiveRow();
|
|
543
561
|
scheduleContextUsageRefresh(conversationId, 120);
|
|
544
562
|
}
|
|
@@ -558,7 +576,7 @@ export async function connectSocket(overrideDeps) {
|
|
|
558
576
|
socket.on('message_updated', ({ conversationId, messageId, message }) => {
|
|
559
577
|
if (!messageId || conversationId !== currentConvId || !message) return;
|
|
560
578
|
if (!replaceRenderedMessage(message, messageId)) {
|
|
561
|
-
appendMessage(message,
|
|
579
|
+
appendMessage(message, followsTranscriptEnd(), messageId, false);
|
|
562
580
|
}
|
|
563
581
|
noteTranscriptLiveRow();
|
|
564
582
|
});
|
|
@@ -578,17 +596,49 @@ export async function connectSocket(overrideDeps) {
|
|
|
578
596
|
});
|
|
579
597
|
socket.on('background_tasks', ({ conversationId, tasks }) => {
|
|
580
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();
|
|
581
606
|
});
|
|
582
607
|
// A message was pinned or unpinned (any client): the conversation's whole
|
|
583
608
|
// pin list, replacing what this page had.
|
|
584
609
|
socket.on('conversation_pins_updated', ({ conversationId, pins, revision }) => {
|
|
585
610
|
setConversationPins(conversationId, pins, revision);
|
|
586
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
|
+
});
|
|
587
617
|
// REPLACE semantics for the whole set: the preview registry is relay-owned
|
|
588
618
|
// and global, so every change ships the full list.
|
|
589
619
|
socket.on('previews', ({ previews }) => {
|
|
590
620
|
setPreviews(previews);
|
|
591
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
|
+
});
|
|
592
642
|
// REPLACE semantics too: the relay ships the whole remote-relay list (no
|
|
593
643
|
// tokens) whenever a remote is added, removed, renamed or changes status.
|
|
594
644
|
socket.on(REMOTE_RELAY_SOCKET_EVENT, (payload) => {
|
|
@@ -613,16 +663,17 @@ export async function connectSocket(overrideDeps) {
|
|
|
613
663
|
const last = items[items.length - 1];
|
|
614
664
|
const lastText = typeof last === 'string' ? last : String(last?.text || '');
|
|
615
665
|
const lastSubagentRunId = typeof last === 'object' && last ? (last.subagentRunId || null) : null;
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
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)));
|
|
619
669
|
if (entry.subagentRunId) {
|
|
620
670
|
upsertSubagentRun({ subagentRunId: entry.subagentRunId, messageId, conversationId });
|
|
621
671
|
addSubagentActivity(entry.subagentRunId, entry.text);
|
|
622
672
|
}
|
|
623
673
|
if (conversationId === currentConvId) {
|
|
624
|
-
const autoScroll =
|
|
674
|
+
const autoScroll = followsTranscriptEnd();
|
|
625
675
|
appendThinkingActivity(entry, entry.subagentRunId, autoScroll);
|
|
676
|
+
if (added && !autoScroll) noteTranscriptNews();
|
|
626
677
|
}
|
|
627
678
|
});
|
|
628
679
|
socket.on('relay_stream', ({ conversationId, messageId, text, done, seq, subagentRunId }) => {
|
|
@@ -631,8 +682,8 @@ export async function connectSocket(overrideDeps) {
|
|
|
631
682
|
upsertSubagentRun({ subagentRunId: String(subagentRunId).trim(), messageId, conversationId });
|
|
632
683
|
}
|
|
633
684
|
if (conversationId !== currentConvId) return;
|
|
634
|
-
const autoScroll =
|
|
635
|
-
applyRelayStreamEvent({
|
|
685
|
+
const autoScroll = followsTranscriptEnd();
|
|
686
|
+
const accepted = applyRelayStreamEvent({
|
|
636
687
|
messageId,
|
|
637
688
|
text: String(text || ''),
|
|
638
689
|
done: !!done,
|
|
@@ -640,11 +691,16 @@ export async function connectSocket(overrideDeps) {
|
|
|
640
691
|
subagentRunId: subagentRunId ? String(subagentRunId).trim() : null,
|
|
641
692
|
autoScroll,
|
|
642
693
|
});
|
|
694
|
+
// A frame that was dropped (out of order, a finished turn) is no news.
|
|
695
|
+
if (accepted && !autoScroll) noteTranscriptNews();
|
|
643
696
|
});
|
|
644
697
|
socket.on('relay_thought', ({ conversationId, messageId, reasoningId, text, done, subagentRunId }) => {
|
|
645
698
|
if (!messageId) return;
|
|
646
699
|
const key = String(reasoningId || 'reasoning');
|
|
647
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;
|
|
648
704
|
thoughtMap.set(key, { reasoningId: key, text: String(text || ''), done: !!done, subagentRunId: subagentRunId || null });
|
|
649
705
|
relayThoughts.set(messageId, thoughtMap);
|
|
650
706
|
if (subagentRunId) {
|
|
@@ -652,8 +708,9 @@ export async function connectSocket(overrideDeps) {
|
|
|
652
708
|
addSubagentThought(subagentRunId, { reasoningId: key, text: String(text || ''), done: !!done });
|
|
653
709
|
}
|
|
654
710
|
if (conversationId === currentConvId) {
|
|
655
|
-
const autoScroll =
|
|
711
|
+
const autoScroll = followsTranscriptEnd();
|
|
656
712
|
appendThinkingThought(key, String(text || ''), !!done, subagentRunId, autoScroll);
|
|
713
|
+
if (added && !autoScroll) noteTranscriptNews();
|
|
657
714
|
}
|
|
658
715
|
});
|
|
659
716
|
socket.on('subagent_status', (payload = {}) => {
|
|
@@ -770,6 +827,9 @@ export async function connectSocket(overrideDeps) {
|
|
|
770
827
|
applyConversationTurnStatus({ conversationId, messageId, status });
|
|
771
828
|
if (conversationId && conversations[conversationId]) {
|
|
772
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);
|
|
773
833
|
if (normalizedStatus === 'processing') {
|
|
774
834
|
conversation.localTurnStatus = 'processing';
|
|
775
835
|
conversation.localTurnStatusUpdatedAt = Date.now();
|
|
@@ -785,7 +845,7 @@ export async function connectSocket(overrideDeps) {
|
|
|
785
845
|
}
|
|
786
846
|
}
|
|
787
847
|
if (conversationId === currentConvId && normalizedStatus === 'processing') {
|
|
788
|
-
const autoScroll =
|
|
848
|
+
const autoScroll = followsTranscriptEnd();
|
|
789
849
|
showThinking(messageId || null, autoScroll);
|
|
790
850
|
renderThinkingActivities();
|
|
791
851
|
renderThinkingThoughts();
|
|
@@ -821,6 +881,12 @@ export async function connectSocket(overrideDeps) {
|
|
|
821
881
|
}
|
|
822
882
|
renderConvList();
|
|
823
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
|
+
});
|
|
824
890
|
socket.on('conversation_archived', ({ conversationId, archived }) => {
|
|
825
891
|
const id = String(conversationId || '').trim();
|
|
826
892
|
if (!id) return;
|
|
@@ -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);
|
|
@@ -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();
|
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
// and drops on a backed-up connection; a missed event shows up as a hole in
|
|
10
10
|
// the seq numbers and is fetched again, and so is the end of a run. Full-screen on
|
|
11
11
|
// phones; Escape, ×, a click outside and the system back close it.
|
|
12
|
+
//
|
|
13
|
+
// A shared view (and the owner's "Preview as shared") opens it read-only: the
|
|
14
|
+
// record comes filtered from the share's sub-agent route, without a socket, so
|
|
15
|
+
// a running agent's record is read again every few seconds; no Stop.
|
|
12
16
|
|
|
13
|
-
import { apiFetch, cancelSubagentRun } from './api-client.js';
|
|
17
|
+
import { apiFetch, cancelSubagentRun, loadSharedSubagentRecord } from './api-client.js';
|
|
14
18
|
import { renderMarkdownPreview } from './router.js';
|
|
15
19
|
import { currentConvId, getSubagentRun, IS_SHARED_VIEW, upsertSubagentRun } from './store.js';
|
|
16
20
|
import { createViewerHistory } from './viewer-history.mjs';
|
|
@@ -30,6 +34,7 @@ import {
|
|
|
30
34
|
const PAGE_LIMIT = 500;
|
|
31
35
|
const MAX_PAGES = 10;
|
|
32
36
|
const STOP_PENDING_MS = 15_000;
|
|
37
|
+
const SHARED_POLL_MS = 3_000;
|
|
33
38
|
|
|
34
39
|
const viewer = {
|
|
35
40
|
open: false,
|
|
@@ -44,6 +49,7 @@ const viewer = {
|
|
|
44
49
|
stopping: false,
|
|
45
50
|
stopTimer: null,
|
|
46
51
|
tickTimer: null,
|
|
52
|
+
sharedPollTimer: null,
|
|
47
53
|
openFolds: new Set(),
|
|
48
54
|
};
|
|
49
55
|
|
|
@@ -146,7 +152,7 @@ function buildEntryNode(entry) {
|
|
|
146
152
|
renderMarkdown,
|
|
147
153
|
children: viewer.children,
|
|
148
154
|
openDetails: viewer.openFolds.has(entry.seq),
|
|
149
|
-
clickable:
|
|
155
|
+
clickable: true,
|
|
150
156
|
});
|
|
151
157
|
return holder.firstElementChild;
|
|
152
158
|
}
|
|
@@ -215,6 +221,21 @@ function applyRunHeader(header) {
|
|
|
215
221
|
}
|
|
216
222
|
renderHeader();
|
|
217
223
|
syncTicker();
|
|
224
|
+
syncSharedPoll();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Without a socket a running record is read again while it is on screen.
|
|
228
|
+
function syncSharedPoll() {
|
|
229
|
+
const want = IS_SHARED_VIEW && viewer.open && runIsRunning(viewer.run);
|
|
230
|
+
if (want && !viewer.sharedPollTimer) {
|
|
231
|
+
viewer.sharedPollTimer = setInterval(() => {
|
|
232
|
+
if (!viewer.loading && document.visibilityState !== 'hidden') void fetchRecord();
|
|
233
|
+
}, SHARED_POLL_MS);
|
|
234
|
+
}
|
|
235
|
+
if (!want && viewer.sharedPollTimer) {
|
|
236
|
+
clearInterval(viewer.sharedPollTimer);
|
|
237
|
+
viewer.sharedPollTimer = null;
|
|
238
|
+
}
|
|
218
239
|
}
|
|
219
240
|
|
|
220
241
|
/**
|
|
@@ -231,7 +252,9 @@ async function fetchRecord({ fromStart = false } = {}) {
|
|
|
231
252
|
renderStatus();
|
|
232
253
|
try {
|
|
233
254
|
for (let page = 0; page < MAX_PAGES; page += 1) {
|
|
234
|
-
const data =
|
|
255
|
+
const data = IS_SHARED_VIEW
|
|
256
|
+
? await loadSharedSubagentRecord(runId, { after, limit: PAGE_LIMIT })
|
|
257
|
+
: await apiFetch(`/api/conversation/${encodeURIComponent(conversationId)}/subagent/${encodeURIComponent(runId)}?after=${after}&limit=${PAGE_LIMIT}`);
|
|
235
258
|
if (token !== viewer.fetchToken || !viewer.open || viewer.state?.runId !== runId) return;
|
|
236
259
|
if (!data) {
|
|
237
260
|
viewer.error = viewer.state.entries.size ? '' : 'This agent\'s record could not be loaded.';
|
|
@@ -272,7 +295,7 @@ async function loadRun(runId) {
|
|
|
272
295
|
renderHeader();
|
|
273
296
|
renderStatus();
|
|
274
297
|
// Joined before the read, so nothing sent in between is lost.
|
|
275
|
-
await viewerWatch.watch(viewer.conversationId, id);
|
|
298
|
+
if (!IS_SHARED_VIEW) await viewerWatch.watch(viewer.conversationId, id);
|
|
276
299
|
if (!viewer.open || viewer.state?.runId !== id) return;
|
|
277
300
|
await fetchRecord({ fromStart: true });
|
|
278
301
|
// A finished record reads from the top; a running one follows its end.
|
|
@@ -317,6 +340,7 @@ export function closeSubagentViewer({ fromHistory = false } = {}) {
|
|
|
317
340
|
viewer.children = new Map();
|
|
318
341
|
clearTimeout(viewer.stopTimer);
|
|
319
342
|
syncTicker();
|
|
343
|
+
syncSharedPoll();
|
|
320
344
|
if (!fromHistory) viewerHistory?.closed();
|
|
321
345
|
}
|
|
322
346
|
|
|
@@ -338,7 +362,7 @@ async function openParent() {
|
|
|
338
362
|
|
|
339
363
|
async function stopRun() {
|
|
340
364
|
const run = viewer.run;
|
|
341
|
-
if (!run || viewer.stopping || !runIsRunning(run)) return;
|
|
365
|
+
if (IS_SHARED_VIEW || !run || viewer.stopping || !runIsRunning(run)) return;
|
|
342
366
|
viewer.stopping = true;
|
|
343
367
|
renderHeader();
|
|
344
368
|
const task = liveTaskFor(run);
|
|
@@ -475,7 +499,7 @@ export function initSubagentViewer() {
|
|
|
475
499
|
// (a child row) opens that child in place.
|
|
476
500
|
document.addEventListener('click', (event) => {
|
|
477
501
|
const trigger = event.target?.closest?.('[data-subagent-open]');
|
|
478
|
-
if (!trigger
|
|
502
|
+
if (!trigger) return;
|
|
479
503
|
event.preventDefault();
|
|
480
504
|
event.stopPropagation();
|
|
481
505
|
const runId = trigger.getAttribute('data-subagent-open');
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
// per conversation for the life of the page.
|
|
8
8
|
|
|
9
9
|
import { isChatInteractionHeld } from './selection-guard.mjs';
|
|
10
|
-
import { AGENT_EXCHANGE_CLOSED_CLASS } from './agent-exchange-fold-view.mjs';
|
|
11
10
|
|
|
12
11
|
export const THINKING_FOLLOW_MODES = Object.freeze(['thoughts', 'stream', 'tools']);
|
|
13
12
|
export const THINKING_FOLLOW_BOTTOM_GAP_PX = 16;
|
|
@@ -109,9 +108,6 @@ function resolveTarget(mode) {
|
|
|
109
108
|
const el = document.querySelector(selector);
|
|
110
109
|
if (!el) return null;
|
|
111
110
|
if (el.hidden || el.closest?.('[hidden]')) return null;
|
|
112
|
-
// Nor does a live bubble folded into an agent exchange: it has no box, and
|
|
113
|
-
// following it would scroll the pane to the top.
|
|
114
|
-
if (el.closest?.(`.${AGENT_EXCHANGE_CLOSED_CLASS}`)) return null;
|
|
115
111
|
// A collapsed <details> or an empty list has nothing to follow yet.
|
|
116
112
|
const details = el.closest?.('details');
|
|
117
113
|
if (details && !details.open) return null;
|