@oar-sh/oar 0.9.21 → 0.9.22
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 +113 -0
- package/README.md +9 -9
- package/package.json +1 -1
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +9 -1
- package/server/claude-worker/claude-sdk-adapter.mjs +149 -4
- package/server/claude-worker/claude-session-process.mjs +9 -4
- package/server/claude-worker/claude-session-worker-main.mjs +10 -1
- package/server/copilot-worker/copilot-sdk-session-process.mjs +1 -0
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +10 -1
- package/server/cursor-worker/cursor-session-worker.mjs +9 -1
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/grok-background-tasks.mjs +1 -0
- package/server/grok-worker/grok-session-worker.mjs +10 -1
- package/server/grok-worker/grok-turn-runner.mjs +1 -0
- package/server/migrations/0037-relay-asks-queue-id.mjs +66 -0
- package/server/migrations/0038-turn-seq.mjs +92 -0
- package/server/public/app/activity-replay-state.mjs +160 -42
- package/server/public/app/api-client.js +7 -0
- package/server/public/app/ask-user-view.js +4 -3
- package/server/public/app/bootstrap.js +62 -4
- package/server/public/app/chat-actions-menu-state.mjs +2 -0
- package/server/public/app/context-usage-view.mjs +68 -0
- package/server/public/app/conversation-view.js +306 -49
- package/server/public/app/header-priority-actions.mjs +5 -1
- package/server/public/app/held-details.mjs +202 -0
- package/server/public/app/inbox-ui.js +70 -79
- package/server/public/app/inbox-view.mjs +35 -19
- package/server/public/app/journal-view.js +17 -0
- package/server/public/app/live-activity-cap.mjs +103 -0
- package/server/public/app/questions-panel-ui.js +634 -0
- package/server/public/app/questions-stub-view.mjs +40 -0
- package/server/public/app/questions-view.mjs +89 -0
- package/server/public/app/relay-board-view.js +12 -2
- package/server/public/app/relay-thoughts.mjs +15 -1
- package/server/public/app/session-history-ui.js +1 -0
- package/server/public/app/settings-modal.js +6 -0
- package/server/public/app/socket-handlers.js +42 -11
- package/server/public/app/stream-view-preference.mjs +60 -0
- package/server/public/app/subagent-fold.mjs +6 -3
- package/server/public/app/subagent-timeline-view.mjs +12 -5
- package/server/public/app/subagent-viewer.mjs +8 -6
- package/server/public/app/thoughts-preference.mjs +79 -0
- package/server/public/app/turn-stream-view.mjs +124 -0
- package/server/public/index.html +391 -214
- package/server/repositories/message-repository.mjs +10 -0
- package/server/repositories/question-repository.mjs +51 -7
- package/server/repositories/relay-turn-rows.mjs +56 -0
- package/server/routes/ask-user-routes.mjs +34 -5
- package/server/routes/messages-routes.mjs +35 -14
- package/server/routes/relay-ask-routes.mjs +15 -5
- package/server/routes/sessions-routes.mjs +76 -5
- package/server/server-runtime.mjs +178 -131
- package/server/services/cloudflared-tunnel-service.mjs +7 -0
- package/server/services/idle-timers.mjs +184 -0
- package/server/services/queue-wake-hook.mjs +145 -0
- package/server/services/queue-waker.mjs +127 -0
- package/server/services/relay-ask-service.mjs +64 -5
- package/server/services/relay-event-service.mjs +4 -1
- package/server/services/remote-relay-dispatcher.mjs +23 -4
- package/server/services/session-history-refresh-service.mjs +4 -0
- package/server/services/session-worker-supervisor-service.mjs +36 -0
- package/server/services/session-worker-websocket-service.mjs +143 -15
- package/server/services/ssh-tunnel-manager-service.mjs +7 -0
- package/shared/ask-user-bridge.mjs +9 -6
- package/shared/relay-asks.mjs +10 -0
- package/shared/relay-events.mjs +1 -0
- package/shared/worker-runtime/heartbeat.mjs +26 -0
- package/shared/worker-runtime/worker-keepalive.mjs +108 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +21 -10
|
@@ -94,6 +94,11 @@ import {
|
|
|
94
94
|
} from './composer-attachment-cache.mjs';
|
|
95
95
|
import { renderRelayQuestions } from './ask-user-view.js';
|
|
96
96
|
import { renderRelayBoards } from './relay-board-view.js';
|
|
97
|
+
import { renderQuestionStubs } from './questions-panel-ui.js';
|
|
98
|
+
import { isThoughtsExpanded, rememberThoughtFoldChoice, thoughtFoldOpen } from './thoughts-preference.mjs';
|
|
99
|
+
import { liveActivityCap } from './live-activity-cap.mjs';
|
|
100
|
+
import { isStreamViewEnabled } from './stream-view-preference.mjs';
|
|
101
|
+
import { isProgressActivityText, renderTurnStreamMarkup, turnStreamEntries } from './turn-stream-view.mjs';
|
|
97
102
|
import { getMessageThreadAnchor, sortConversationMessages } from './thread-order.mjs';
|
|
98
103
|
import {
|
|
99
104
|
normalizeStreamSeq,
|
|
@@ -102,11 +107,13 @@ import {
|
|
|
102
107
|
computeNextRelayStreamState,
|
|
103
108
|
} from './stream-state.mjs';
|
|
104
109
|
import {
|
|
105
|
-
|
|
110
|
+
capFinishedActivityEntries,
|
|
106
111
|
compactBoundaryFromActivities,
|
|
112
|
+
isOmittedActivityMarker,
|
|
107
113
|
isCompactBoundaryActivityEntry,
|
|
108
114
|
mergeRelayActivityTexts,
|
|
109
115
|
normalizeRelayActivityEntry,
|
|
116
|
+
normalizeTurnSeq,
|
|
110
117
|
promotedCompactBoundaryEntry,
|
|
111
118
|
relayActivityEntryText,
|
|
112
119
|
visibleActivityEntries,
|
|
@@ -154,7 +161,7 @@ import { createHeldBackLiveRows, newerPageReachesEnd } from './transcript-island
|
|
|
154
161
|
import { buildInFlightSnapshotKey } from './in-flight-snapshot.mjs';
|
|
155
162
|
import { computeStablePrefixLength, planListPatch } from './streaming-dom-patch.mjs';
|
|
156
163
|
import { prefixToolActivityEmoji } from './tool-activity-emoji.mjs';
|
|
157
|
-
import { conversationPayloadCache } from './conversation-payload-cache.mjs';
|
|
164
|
+
import { conversationPayloadCache, windowPageInfo } from './conversation-payload-cache.mjs';
|
|
158
165
|
import { noteConversationShown } from './session-history-ui.js';
|
|
159
166
|
|
|
160
167
|
const CONVERSATION_HISTORY_PAGE_SIZE = 20;
|
|
@@ -1613,9 +1620,10 @@ function createMessageNode(msg, msgId = null, force = false) {
|
|
|
1613
1620
|
const content = renderMarkdownPreview(msg.text || '', false);
|
|
1614
1621
|
const attachments = Array.isArray(msg.attachments) ? msg.attachments : [];
|
|
1615
1622
|
// capRelayActivityEntries, not slice: a boundary past the cap would take
|
|
1616
|
-
// the break row with it.
|
|
1623
|
+
// the break row with it. The relay's cap of a finished turn (newest rows
|
|
1624
|
+
// per thread), so an older relay's longer list reads the same.
|
|
1617
1625
|
const activities = Array.isArray(msg.activities)
|
|
1618
|
-
?
|
|
1626
|
+
? capFinishedActivityEntries(msg.activities.filter(Boolean))
|
|
1619
1627
|
: [];
|
|
1620
1628
|
// A compaction that happened during this turn is rendered as a full-width
|
|
1621
1629
|
// break row immediately above this message (syncTranscriptSeparators reads
|
|
@@ -1634,8 +1642,27 @@ function createMessageNode(msg, msgId = null, force = false) {
|
|
|
1634
1642
|
const subagentRuns = Array.isArray(msg.subagentRuns) ? msg.subagentRuns.filter(Boolean) : [];
|
|
1635
1643
|
const mainActivities = activities.filter((item) => !(normalizeRelayActivityEntry(item)?.subagentRunId));
|
|
1636
1644
|
const mainThoughts = thoughts.filter((t) => !t?.subagentRunId);
|
|
1637
|
-
|
|
1638
|
-
|
|
1645
|
+
// Stream view: the turn's thoughts, tool lines and reply as one list in
|
|
1646
|
+
// the order they happened, when the turn's rows carry their order (null:
|
|
1647
|
+
// the grouped layout below, unchanged). Progress lines ('● ') stay where
|
|
1648
|
+
// the grouped layout puts them.
|
|
1649
|
+
const streamEntries = msg.role === 'assistant' && isStreamViewEnabled()
|
|
1650
|
+
? turnStreamEntries({
|
|
1651
|
+
thoughts: mainThoughts,
|
|
1652
|
+
activities: visibleActivityEntries(mainActivities, promotedBoundaryEntry),
|
|
1653
|
+
text: msg.text || '',
|
|
1654
|
+
})
|
|
1655
|
+
: null;
|
|
1656
|
+
const activityHtml = streamEntries
|
|
1657
|
+
? renderActivityMarkup(mainActivities.filter((item) => isProgressActivityText(relayActivityEntryText(item))), promotedBoundaryEntry)
|
|
1658
|
+
: (mainActivities.length ? renderActivityMarkup(mainActivities, promotedBoundaryEntry) : '');
|
|
1659
|
+
const thoughtsHtml = !streamEntries && mainThoughts.length ? renderThoughtsMarkup(mainThoughts) : '';
|
|
1660
|
+
const replyHtml = streamEntries
|
|
1661
|
+
? renderTurnStreamMarkup(streamEntries, {
|
|
1662
|
+
renderMarkdown: (text) => renderMarkdownPreview(text, false),
|
|
1663
|
+
decorate: decorateActivityText,
|
|
1664
|
+
})
|
|
1665
|
+
: content;
|
|
1639
1666
|
// One row per sub-agent the turn spawned, as in the composer's task panel;
|
|
1640
1667
|
// its title opens the agent's whole record. The store's copy wins: status
|
|
1641
1668
|
// events keep it current after the bubble was built.
|
|
@@ -1720,7 +1747,7 @@ function createMessageNode(msg, msgId = null, force = false) {
|
|
|
1720
1747
|
: '';
|
|
1721
1748
|
|
|
1722
1749
|
div.innerHTML = `
|
|
1723
|
-
<div class="${bubbleClass}">${shareVisibilityActionHtml}${thoughtsHtml}${originBadgeHtml}${
|
|
1750
|
+
<div class="${bubbleClass}">${shareVisibilityActionHtml}${thoughtsHtml}${originBadgeHtml}${replyHtml}${relayErrorCtaHtml}${attachmentHtml}${activityHtml}${subagentHtml}${workflowRunsHtml}${previewCardsHtml}${userBubbleActionsHtml}${stoppedSteerActionsHtml}${workerLaunchActionsHtml}</div>
|
|
1724
1751
|
<div class="msg-label">${label}${modelTag}${reasoningTag}${modeTag}${autoTag}${continuationTag}${crossProviderTag}${usageTurnTag}${usageRemainingTag}${usageStaleTag} · ${fmtDate(msg.timestamp)}</div>`;
|
|
1725
1752
|
|
|
1726
1753
|
const bubble = div.querySelector('.msg-bubble');
|
|
@@ -1915,10 +1942,12 @@ export function renderActivityMarkup(activities, promotedBoundaryEntry = undefin
|
|
|
1915
1942
|
const progressHtml = progress.length
|
|
1916
1943
|
? `<div class="msg-activity-list">${progress.map((item) => `<div class="msg-activity-item">${escHtml(decorateActivityText(relayActivityEntryText(item)))}</div>`).join('')}</div>`
|
|
1917
1944
|
: '';
|
|
1945
|
+
// The "… N earlier tool lines omitted" row is listed but is no tool line.
|
|
1946
|
+
const toolCount = tools.filter((item) => !isOmittedActivityMarker(item)).length;
|
|
1918
1947
|
const toolsHtml = tools.length
|
|
1919
1948
|
? `
|
|
1920
1949
|
<details class="msg-activity">
|
|
1921
|
-
<summary>🔧 Tool activity (${
|
|
1950
|
+
<summary>🔧 Tool activity (${toolCount})</summary>
|
|
1922
1951
|
<div class="msg-activity-list">${tools.map((item) => `<div class="msg-activity-item">${escHtml(decorateActivityText(relayActivityEntryText(item)))}</div>`).join('')}</div>
|
|
1923
1952
|
</details>`
|
|
1924
1953
|
: '';
|
|
@@ -2012,6 +2041,7 @@ export function showThinking(messageId = null, autoScroll = true) {
|
|
|
2012
2041
|
<summary>💭 Thoughts</summary>
|
|
2013
2042
|
<div class="thinking-thoughts-list"></div>
|
|
2014
2043
|
</details>
|
|
2044
|
+
${isStreamViewEnabled() ? '<div id="thinking-stream-list" class="thinking-stream-list"></div>' : ''}
|
|
2015
2045
|
<div id="thinking-stream" class="thinking-stream" hidden></div>
|
|
2016
2046
|
<div class="thinking-compacting-note" hidden>Compacting the conversation…</div>
|
|
2017
2047
|
<div class="dots"><span></span><span></span><span></span></div>
|
|
@@ -2022,8 +2052,10 @@ export function showThinking(messageId = null, autoScroll = true) {
|
|
|
2022
2052
|
syncThinkingCompaction(div);
|
|
2023
2053
|
renderThinkingThoughts();
|
|
2024
2054
|
renderThinkingStream();
|
|
2025
|
-
// A board the turn has posted already lives in its live bubble
|
|
2055
|
+
// A board the turn has posted already lives in its live bubble, and so
|
|
2056
|
+
// does the 📓 stub of an ask it made.
|
|
2026
2057
|
renderRelayBoards();
|
|
2058
|
+
renderQuestionStubs();
|
|
2027
2059
|
observeThinkingBubble(div);
|
|
2028
2060
|
if (getThinkingFollowMode()) applyThinkingFollow();
|
|
2029
2061
|
else if (autoScroll) scrollBottom();
|
|
@@ -2158,7 +2190,7 @@ export function collapseThinkingThoughts() {
|
|
|
2158
2190
|
if (!(panel instanceof HTMLDetailsElement)) return;
|
|
2159
2191
|
panel.open = false;
|
|
2160
2192
|
panel.querySelectorAll('.thinking-thought').forEach((row) => {
|
|
2161
|
-
if (row instanceof HTMLDetailsElement) row
|
|
2193
|
+
if (row instanceof HTMLDetailsElement) showLiveThoughtRow(row, false);
|
|
2162
2194
|
});
|
|
2163
2195
|
}
|
|
2164
2196
|
|
|
@@ -2252,6 +2284,7 @@ export function renderThinkingActivities() {
|
|
|
2252
2284
|
// clear-and-replay (or the append path's last-row-only dedupe) would either
|
|
2253
2285
|
// wipe selections or duplicate rows on every changed payload.
|
|
2254
2286
|
const mainExpected = [];
|
|
2287
|
+
const mainEntries = [];
|
|
2255
2288
|
const bySubagentRun = new Map();
|
|
2256
2289
|
for (const item of items) {
|
|
2257
2290
|
const entry = normalizeRelayActivityEntry(item);
|
|
@@ -2261,15 +2294,22 @@ export function renderThinkingActivities() {
|
|
|
2261
2294
|
// would otherwise flip presentation the moment the turn lands.
|
|
2262
2295
|
if (isCompactBoundaryActivityEntry(entry)) continue;
|
|
2263
2296
|
const decorated = decorateActivityText(entry.text);
|
|
2297
|
+
// A numbered row is its own line even when the same tool line came just
|
|
2298
|
+
// before (the same call twice); an unnumbered repeat is one line.
|
|
2299
|
+
const numbered = entry.turnSeq !== undefined;
|
|
2264
2300
|
if (entry.subagentRunId) {
|
|
2265
2301
|
const list = bySubagentRun.get(entry.subagentRunId) || [];
|
|
2266
|
-
if (list[list.length - 1] !== decorated) list.push(decorated);
|
|
2302
|
+
if (numbered || list[list.length - 1] !== decorated) list.push(decorated);
|
|
2267
2303
|
bySubagentRun.set(entry.subagentRunId, list);
|
|
2268
|
-
} else if (mainExpected[mainExpected.length - 1] !== decorated) {
|
|
2304
|
+
} else if (numbered || mainExpected[mainExpected.length - 1] !== decorated) {
|
|
2269
2305
|
mainExpected.push(decorated);
|
|
2306
|
+
mainEntries.push({ text: decorated, turnSeq: entry.turnSeq ?? null });
|
|
2270
2307
|
}
|
|
2271
2308
|
}
|
|
2272
|
-
|
|
2309
|
+
const streamList = liveStreamList();
|
|
2310
|
+
if (streamList) {
|
|
2311
|
+
if (!selectionIntersectsNode(streamList)) patchStreamListActivities(streamList, mainEntries);
|
|
2312
|
+
} else if (!selectionIntersectsNode(box)) {
|
|
2273
2313
|
patchActivityList(box, mainExpected, 'thinking-activity-item');
|
|
2274
2314
|
}
|
|
2275
2315
|
syncThinkingCompaction();
|
|
@@ -2297,7 +2337,9 @@ export function restoreInFlightThinking(inFlight, autoScroll = true) {
|
|
|
2297
2337
|
// live poll would otherwise churn the DOM (and the user's selection) with
|
|
2298
2338
|
// identical content. The indicator must belong to this turn's message —
|
|
2299
2339
|
// skipping on a foreign bubble would let a misplaced one stick around.
|
|
2300
|
-
|
|
2340
|
+
// The cap is part of it: a changed "Tool lines shown" refills or trims the
|
|
2341
|
+
// bubble on the next poll even when the payload is the same.
|
|
2342
|
+
const snapshotKey = `${liveActivityCap()}|${buildInFlightSnapshotKey(inFlight)}`;
|
|
2301
2343
|
const liveIndicator = document.getElementById('thinking-indicator');
|
|
2302
2344
|
if (snapshotKey === lastInFlightSnapshotKey
|
|
2303
2345
|
&& liveIndicator
|
|
@@ -2313,6 +2355,7 @@ export function restoreInFlightThinking(inFlight, autoScroll = true) {
|
|
|
2313
2355
|
const activities = mergeRelayActivityTexts(
|
|
2314
2356
|
relayActivities.get(messageId) || [],
|
|
2315
2357
|
Array.isArray(inFlight.activities) ? inFlight.activities : [],
|
|
2358
|
+
liveActivityCap(),
|
|
2316
2359
|
);
|
|
2317
2360
|
relayActivities.set(messageId, activities);
|
|
2318
2361
|
const inFlightThoughts = Array.isArray(inFlight.thoughts) ? inFlight.thoughts : [];
|
|
@@ -2320,7 +2363,15 @@ export function restoreInFlightThinking(inFlight, autoScroll = true) {
|
|
|
2320
2363
|
const thoughtMap = relayThoughts.get(messageId) || new Map();
|
|
2321
2364
|
for (const entry of inFlightThoughts) {
|
|
2322
2365
|
const key = String(entry?.reasoningId || `seq-${entry?.seq || thoughtMap.size}`);
|
|
2323
|
-
|
|
2366
|
+
// A thought keeps the place of its first chunk.
|
|
2367
|
+
const turnSeq = thoughtMap.get(key)?.turnSeq ?? normalizeTurnSeq(entry?.turnSeq);
|
|
2368
|
+
thoughtMap.set(key, {
|
|
2369
|
+
reasoningId: key,
|
|
2370
|
+
text: String(entry?.text || ''),
|
|
2371
|
+
done: !!entry?.done,
|
|
2372
|
+
subagentRunId: entry?.subagentRunId || null,
|
|
2373
|
+
...(turnSeq != null ? { turnSeq } : {}),
|
|
2374
|
+
});
|
|
2324
2375
|
}
|
|
2325
2376
|
relayThoughts.set(messageId, thoughtMap);
|
|
2326
2377
|
}
|
|
@@ -2405,6 +2456,7 @@ export function appendThinkingActivity(item, subagentRunId = null, autoScroll =
|
|
|
2405
2456
|
row.className = 'subagent-activity-item';
|
|
2406
2457
|
row.textContent = decorated;
|
|
2407
2458
|
activityBox.appendChild(row);
|
|
2459
|
+
trimLiveActivityRows(activityBox, 'subagent-activity-item');
|
|
2408
2460
|
refreshSubagentSummary(subagentBubble);
|
|
2409
2461
|
}
|
|
2410
2462
|
}
|
|
@@ -2415,18 +2467,105 @@ export function appendThinkingActivity(item, subagentRunId = null, autoScroll =
|
|
|
2415
2467
|
return;
|
|
2416
2468
|
}
|
|
2417
2469
|
|
|
2418
|
-
|
|
2470
|
+
// Stream view: the main agent's tool lines share one list with its
|
|
2471
|
+
// thoughts, in the turn's order.
|
|
2472
|
+
const streamList = liveStreamList();
|
|
2473
|
+
const box = streamList || document.getElementById('thinking-activity');
|
|
2419
2474
|
if (!box) return;
|
|
2420
|
-
const
|
|
2421
|
-
if (
|
|
2475
|
+
const numbered = entry.turnSeq !== undefined;
|
|
2476
|
+
if (numbered) {
|
|
2477
|
+
// A numbered line is known by its number: the same call twice is two.
|
|
2478
|
+
if (box.querySelector(`.thinking-activity-item[data-turn-seq="${entry.turnSeq}"]`)) return;
|
|
2479
|
+
} else if (streamList) {
|
|
2480
|
+
const rows = streamList.querySelectorAll(':scope > .thinking-activity-item');
|
|
2481
|
+
if ((rows[rows.length - 1]?.textContent || '') === decorated) return;
|
|
2482
|
+
} else if ((box.lastElementChild?.textContent || '') === decorated) {
|
|
2483
|
+
return;
|
|
2484
|
+
}
|
|
2422
2485
|
const row = document.createElement('div');
|
|
2423
2486
|
row.className = 'thinking-activity-item';
|
|
2424
|
-
if (
|
|
2487
|
+
if (numbered) row.dataset.turnSeq = String(entry.turnSeq);
|
|
2425
2488
|
row.textContent = decorated;
|
|
2426
|
-
|
|
2489
|
+
if (streamList) placeByTurnSeq(streamList, row);
|
|
2490
|
+
else box.appendChild(row);
|
|
2491
|
+
trimLiveActivityRows(box, 'thinking-activity-item');
|
|
2427
2492
|
if (autoScroll) scrollBottom();
|
|
2428
2493
|
}
|
|
2429
2494
|
|
|
2495
|
+
// The live bubble's single list of thoughts and tool lines, while the stream
|
|
2496
|
+
// view is on (the bubble is rebuilt when it is switched).
|
|
2497
|
+
function liveStreamList() {
|
|
2498
|
+
return isStreamViewEnabled() ? document.getElementById('thinking-stream-list') : null;
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
// The stream list keeps its rows in the turn's order: a numbered row goes
|
|
2502
|
+
// before the first row with a higher number, an unnumbered one (an older
|
|
2503
|
+
// relay, a thought not yet numbered) at the end.
|
|
2504
|
+
function placeByTurnSeq(list, row) {
|
|
2505
|
+
const seq = Number(row.dataset.turnSeq);
|
|
2506
|
+
if (!list.id || list.id !== 'thinking-stream-list' || !Number.isFinite(seq)) {
|
|
2507
|
+
if (row.parentNode !== list) list.appendChild(row);
|
|
2508
|
+
return;
|
|
2509
|
+
}
|
|
2510
|
+
for (const child of list.children) {
|
|
2511
|
+
if (child === row) continue;
|
|
2512
|
+
const childSeq = Number(child.dataset.turnSeq);
|
|
2513
|
+
if (Number.isFinite(childSeq) && childSeq > seq) {
|
|
2514
|
+
if (row.nextSibling !== child) list.insertBefore(row, child);
|
|
2515
|
+
return;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
if (row.parentNode !== list || list.lastElementChild !== row) list.appendChild(row);
|
|
2519
|
+
}
|
|
2520
|
+
|
|
2521
|
+
/**
|
|
2522
|
+
* The poll replay of the stream list's tool lines: `expected` is the main
|
|
2523
|
+
* agent's lines as the turn holds them now ({ text, turnSeq }, already
|
|
2524
|
+
* capped). Rows no longer expected go, missing ones are placed by number;
|
|
2525
|
+
* thoughts are left where they are.
|
|
2526
|
+
*/
|
|
2527
|
+
function patchStreamListActivities(list, expected) {
|
|
2528
|
+
const rows = [...list.querySelectorAll(':scope > .thinking-activity-item')];
|
|
2529
|
+
if (expected.every((entry) => entry.turnSeq !== null)) {
|
|
2530
|
+
const wanted = new Map(expected.map((entry) => [String(entry.turnSeq), entry]));
|
|
2531
|
+
for (const row of rows) {
|
|
2532
|
+
if (!wanted.has(row.dataset.turnSeq || '')) row.remove();
|
|
2533
|
+
}
|
|
2534
|
+
for (const entry of expected) {
|
|
2535
|
+
const key = String(entry.turnSeq);
|
|
2536
|
+
let row = list.querySelector(`:scope > .thinking-activity-item[data-turn-seq="${key}"]`);
|
|
2537
|
+
if (!row) {
|
|
2538
|
+
row = document.createElement('div');
|
|
2539
|
+
row.className = 'thinking-activity-item';
|
|
2540
|
+
row.dataset.turnSeq = key;
|
|
2541
|
+
}
|
|
2542
|
+
if (row.textContent !== entry.text) row.textContent = entry.text;
|
|
2543
|
+
placeByTurnSeq(list, row);
|
|
2544
|
+
}
|
|
2545
|
+
return;
|
|
2546
|
+
}
|
|
2547
|
+
// Unnumbered lines (an older relay): in arrival order after the rest.
|
|
2548
|
+
if (rows.length === expected.length && rows.every((row, index) => row.textContent === expected[index].text)) return;
|
|
2549
|
+
for (const row of rows) row.remove();
|
|
2550
|
+
for (const entry of expected) {
|
|
2551
|
+
const row = document.createElement('div');
|
|
2552
|
+
row.className = 'thinking-activity-item';
|
|
2553
|
+
if (entry.turnSeq !== null) row.dataset.turnSeq = String(entry.turnSeq);
|
|
2554
|
+
row.textContent = entry.text;
|
|
2555
|
+
list.appendChild(row);
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// A line that arrives between two poll replays is appended at once; the
|
|
2560
|
+
// thread's oldest lines beyond the "Tool lines shown" setting go with it, as
|
|
2561
|
+
// the next replay would drop them (each box holds one thread's lines).
|
|
2562
|
+
function trimLiveActivityRows(box, className) {
|
|
2563
|
+
const cap = liveActivityCap();
|
|
2564
|
+
if (!box || !(cap > 0) || selectionIntersectsNode(box)) return;
|
|
2565
|
+
const rows = box.querySelectorAll(`:scope > .${className}`);
|
|
2566
|
+
for (let index = 0; index < rows.length - cap; index += 1) rows[index].remove();
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2430
2569
|
function getSubagentDisplayName(subagentRunId) {
|
|
2431
2570
|
const entry = typeof getSubagentRun === 'function' ? getSubagentRun(subagentRunId) : null;
|
|
2432
2571
|
if (entry?.displayName) return entry.displayName;
|
|
@@ -2568,7 +2707,11 @@ function ensureSubagentBubble(subagentRunId, depth = 0) {
|
|
|
2568
2707
|
}
|
|
2569
2708
|
}
|
|
2570
2709
|
if (entry?.activities?.length) {
|
|
2571
|
-
|
|
2710
|
+
// The run's newest lines only, as many as the "Tool lines shown" setting
|
|
2711
|
+
// keeps per agent.
|
|
2712
|
+
const cap = liveActivityCap();
|
|
2713
|
+
const replayed = cap > 0 ? entry.activities.slice(-cap) : entry.activities;
|
|
2714
|
+
for (const item of replayed) {
|
|
2572
2715
|
const text = typeof item === 'string' ? item : String(item?.text || '').trim();
|
|
2573
2716
|
if (!text) continue;
|
|
2574
2717
|
const decorated = decorateActivityText(text);
|
|
@@ -2595,6 +2738,7 @@ function refreshSubagentSummary(bubble) {
|
|
|
2595
2738
|
const text = subagentFoldSummary({
|
|
2596
2739
|
activities: Array.from(bubble.querySelectorAll(':scope > .subagent-activity > .subagent-activity-item'), (row) => row.textContent || ''),
|
|
2597
2740
|
streamText: bubble.querySelector(':scope > .subagent-stream')?.textContent || '',
|
|
2741
|
+
cap: liveActivityCap(),
|
|
2598
2742
|
});
|
|
2599
2743
|
if (summary.textContent !== text) summary.textContent = text;
|
|
2600
2744
|
}
|
|
@@ -2717,17 +2861,95 @@ function thoughtSummaryText(text) {
|
|
|
2717
2861
|
return value.length > 80 ? `${value.slice(0, 80)}…` : value;
|
|
2718
2862
|
}
|
|
2719
2863
|
|
|
2864
|
+
/**
|
|
2865
|
+
* A chunk only updates the row's state: whether it shows open was decided
|
|
2866
|
+
* when the row was made (the "Expand thoughts" setting or the reader's own
|
|
2867
|
+
* fold), so a thought folded by hand stays folded as more text streams in.
|
|
2868
|
+
*/
|
|
2720
2869
|
export function setLiveThinkingThoughtState(row, done = false) {
|
|
2721
2870
|
if (!row) return;
|
|
2722
2871
|
row.dataset.done = done ? '1' : '0';
|
|
2723
|
-
// The temporary panel represents an active turn, so every thought remains visible
|
|
2724
|
-
// until the completed assistant message replaces it with collapsed history.
|
|
2725
|
-
row.open = true;
|
|
2726
2872
|
}
|
|
2727
2873
|
|
|
2728
|
-
|
|
2874
|
+
// What the reader chose by hand for a live thought ('open' | 'folded'), by
|
|
2875
|
+
// turn, sub-agent run and reasoning id. Kept outside the DOM: the live bubble
|
|
2876
|
+
// is rebuilt on a conversation switch and on a poll replay, and the choice
|
|
2877
|
+
// must survive it. Main-thread reasoning ids are positional within a turn
|
|
2878
|
+
// (the first thought of every turn has the same id), so the turn is part of
|
|
2879
|
+
// the key; a sub-agent run id is unique already and its rows are keyed
|
|
2880
|
+
// without a turn, so a row rebuilt from the store finds its choice.
|
|
2881
|
+
const liveThoughtFoldChoices = new Map();
|
|
2882
|
+
|
|
2883
|
+
/** The turn part of a live thought's key: the queue message, '' for a sub-agent row. */
|
|
2884
|
+
function liveThoughtTurnId(messageId, subagentRunId = null) {
|
|
2885
|
+
return subagentRunId ? '' : String(messageId || '');
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
function liveThoughtFoldKey(turnId, reasoningId, subagentRunId = null) {
|
|
2889
|
+
return `${String(turnId || '')}\u0000${String(subagentRunId || '')}\u0000${String(reasoningId || '')}`;
|
|
2890
|
+
}
|
|
2891
|
+
|
|
2892
|
+
/** Open or folded for a live thought row being made (exported for tests). */
|
|
2893
|
+
export function liveThoughtStartsOpen(reasoningId, subagentRunId = null, turnId = '') {
|
|
2894
|
+
return thoughtFoldOpen({
|
|
2895
|
+
choice: liveThoughtFoldChoices.get(liveThoughtFoldKey(turnId, reasoningId, subagentRunId)) || null,
|
|
2896
|
+
expanded: isThoughtsExpanded(),
|
|
2897
|
+
});
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
/**
|
|
2901
|
+
* Show a live thought row open or folded without that counting as the
|
|
2902
|
+
* reader's choice. `dataset.shownOpen` is what the page last set (or saw the
|
|
2903
|
+
* reader set); a toggle event that disagrees with it came from the reader.
|
|
2904
|
+
*/
|
|
2905
|
+
export function showLiveThoughtRow(row, open) {
|
|
2906
|
+
if (!row) return;
|
|
2907
|
+
row.dataset.shownOpen = open ? '1' : '0';
|
|
2908
|
+
if (row.open !== !!open) row.open = !!open;
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
/** The toggle listener of a live thought row (exported for tests). */
|
|
2912
|
+
export function noteLiveThoughtToggle(row) {
|
|
2913
|
+
if (!row?.dataset) return;
|
|
2914
|
+
const open = !!row.open;
|
|
2915
|
+
if (row.dataset.shownOpen === (open ? '1' : '0')) return;
|
|
2916
|
+
row.dataset.shownOpen = open ? '1' : '0';
|
|
2917
|
+
rememberThoughtFoldChoice(
|
|
2918
|
+
liveThoughtFoldChoices,
|
|
2919
|
+
liveThoughtFoldKey(row.dataset.turnId || '', row.dataset.reasoningId, row.dataset.subagentRunId || null),
|
|
2920
|
+
open,
|
|
2921
|
+
);
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
/**
|
|
2925
|
+
* A live thought row, shown open or folded as the reader's choice or the
|
|
2926
|
+
* setting says. `turnId` comes from the caller (the frame's message id), never
|
|
2927
|
+
* from the live bubble's state, so a rebuilt row gets the same key.
|
|
2928
|
+
*/
|
|
2929
|
+
export function createLiveThoughtRow(key, subagentRunId = null, turnId = '') {
|
|
2930
|
+
const row = document.createElement('details');
|
|
2931
|
+
row.className = 'thinking-thought';
|
|
2932
|
+
row.dataset.reasoningId = key;
|
|
2933
|
+
row.dataset.turnId = String(turnId || '');
|
|
2934
|
+
if (subagentRunId) row.dataset.subagentRunId = subagentRunId;
|
|
2935
|
+
showLiveThoughtRow(row, liveThoughtStartsOpen(key, subagentRunId, row.dataset.turnId));
|
|
2936
|
+
row.addEventListener('toggle', () => noteLiveThoughtToggle(row));
|
|
2937
|
+
const summary = document.createElement('summary');
|
|
2938
|
+
const body = document.createElement('div');
|
|
2939
|
+
body.className = 'thinking-thought-body';
|
|
2940
|
+
row.appendChild(summary);
|
|
2941
|
+
row.appendChild(body);
|
|
2942
|
+
return row;
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
/**
|
|
2946
|
+
* Add or update a live thought. `messageId` is the turn the frame belongs to
|
|
2947
|
+
* (the key of the reader's fold choice for a main-thread thought).
|
|
2948
|
+
*/
|
|
2949
|
+
export function appendThinkingThought(reasoningId, text, done = false, subagentRunId = null, autoScroll = true, messageId = null) {
|
|
2729
2950
|
const key = String(reasoningId || 'reasoning');
|
|
2730
2951
|
const value = String(text || '');
|
|
2952
|
+
const turnId = liveThoughtTurnId(messageId, subagentRunId);
|
|
2731
2953
|
|
|
2732
2954
|
if (subagentRunId) {
|
|
2733
2955
|
const subagentBubble = ensureSubagentBubble(subagentRunId);
|
|
@@ -2742,16 +2964,7 @@ export function appendThinkingThought(reasoningId, text, done = false, subagentR
|
|
|
2742
2964
|
}
|
|
2743
2965
|
let row = thoughtsBox.querySelector(`.thinking-thought[data-reasoning-id="${CSS.escape(key)}"]`);
|
|
2744
2966
|
if (!row) {
|
|
2745
|
-
row =
|
|
2746
|
-
row.className = 'thinking-thought';
|
|
2747
|
-
row.open = !done;
|
|
2748
|
-
row.dataset.reasoningId = key;
|
|
2749
|
-
row.dataset.subagentRunId = subagentRunId;
|
|
2750
|
-
const summary = document.createElement('summary');
|
|
2751
|
-
const body = document.createElement('div');
|
|
2752
|
-
body.className = 'thinking-thought-body';
|
|
2753
|
-
row.appendChild(summary);
|
|
2754
|
-
row.appendChild(body);
|
|
2967
|
+
row = createLiveThoughtRow(key, subagentRunId, turnId);
|
|
2755
2968
|
thoughtsBox.appendChild(row);
|
|
2756
2969
|
}
|
|
2757
2970
|
const summaryEl = row.querySelector('summary');
|
|
@@ -2766,22 +2979,23 @@ export function appendThinkingThought(reasoningId, text, done = false, subagentR
|
|
|
2766
2979
|
return;
|
|
2767
2980
|
}
|
|
2768
2981
|
|
|
2769
|
-
|
|
2982
|
+
// Stream view: the main agent's thoughts share one list with its tool
|
|
2983
|
+
// lines, in the turn's order (the number the store keeps for the thought).
|
|
2984
|
+
const streamList = liveStreamList();
|
|
2985
|
+
const box = streamList || document.querySelector('#thinking-thoughts > .thinking-thoughts-list');
|
|
2770
2986
|
if (!box) return;
|
|
2771
2987
|
let row = box.querySelector(`.thinking-thought[data-reasoning-id="${CSS.escape(key)}"]`);
|
|
2772
2988
|
if (!row) {
|
|
2773
|
-
row =
|
|
2774
|
-
row.className = 'thinking-thought';
|
|
2775
|
-
row.open = !done;
|
|
2776
|
-
row.dataset.reasoningId = key;
|
|
2777
|
-
if (subagentRunId) row.dataset.subagentRunId = subagentRunId;
|
|
2778
|
-
const summary = document.createElement('summary');
|
|
2779
|
-
const body = document.createElement('div');
|
|
2780
|
-
body.className = 'thinking-thought-body';
|
|
2781
|
-
row.appendChild(summary);
|
|
2782
|
-
row.appendChild(body);
|
|
2989
|
+
row = createLiveThoughtRow(key, subagentRunId, turnId);
|
|
2783
2990
|
box.appendChild(row);
|
|
2784
2991
|
}
|
|
2992
|
+
if (streamList) {
|
|
2993
|
+
const turnSeq = relayThoughts.get(String(messageId || thinkingMessageId || ''))?.get(key)?.turnSeq;
|
|
2994
|
+
if (turnSeq != null && row.dataset.turnSeq !== String(turnSeq)) {
|
|
2995
|
+
row.dataset.turnSeq = String(turnSeq);
|
|
2996
|
+
placeByTurnSeq(streamList, row);
|
|
2997
|
+
}
|
|
2998
|
+
}
|
|
2785
2999
|
const summaryEl = row.querySelector('summary');
|
|
2786
3000
|
const bodyEl = row.querySelector('.thinking-thought-body');
|
|
2787
3001
|
if (summaryEl) summaryEl.textContent = `💭 ${thoughtSummaryText(value)}`;
|
|
@@ -2796,7 +3010,7 @@ export function renderThinkingThoughts() {
|
|
|
2796
3010
|
const thoughtMap = thinkingMessageId ? relayThoughts.get(thinkingMessageId) : null;
|
|
2797
3011
|
if (!thoughtMap || !thoughtMap.size) return;
|
|
2798
3012
|
for (const entry of thoughtMap.values()) {
|
|
2799
|
-
appendThinkingThought(entry.reasoningId, entry.text, entry.done, entry.subagentRunId || null, false);
|
|
3013
|
+
appendThinkingThought(entry.reasoningId, entry.text, entry.done, entry.subagentRunId || null, false, thinkingMessageId);
|
|
2800
3014
|
}
|
|
2801
3015
|
}
|
|
2802
3016
|
|
|
@@ -3736,8 +3950,12 @@ export function appendMessage(msg, scroll = true, msgId = null, force = false, i
|
|
|
3736
3950
|
}
|
|
3737
3951
|
}
|
|
3738
3952
|
if (isNewNode && insertedNode) {
|
|
3739
|
-
// A reply takes over the board its turn posted (relay-board-view)
|
|
3740
|
-
|
|
3953
|
+
// A reply takes over the board its turn posted (relay-board-view), and
|
|
3954
|
+
// the 📓 stubs of its asks.
|
|
3955
|
+
if (msg?.role === 'assistant') {
|
|
3956
|
+
renderRelayBoards();
|
|
3957
|
+
renderQuestionStubs();
|
|
3958
|
+
}
|
|
3741
3959
|
syncSeparatorsNow();
|
|
3742
3960
|
if (scroll) scrollBottom();
|
|
3743
3961
|
}
|
|
@@ -3771,6 +3989,8 @@ function buildMessageSnapshotKey(messages = [], meta = {}) {
|
|
|
3771
3989
|
conversationId,
|
|
3772
3990
|
hasMoreOlder: !!hasMoreOlder,
|
|
3773
3991
|
hasMoreNewer: !!hasMoreNewer,
|
|
3992
|
+
// The stream view lays the same payload out differently.
|
|
3993
|
+
streamView: isStreamViewEnabled(),
|
|
3774
3994
|
messages: (Array.isArray(messages) ? messages : []).map((item) => ({
|
|
3775
3995
|
id: String(item?.id || '').trim(),
|
|
3776
3996
|
role: String(item?.role || '').trim(),
|
|
@@ -3798,7 +4018,11 @@ function buildMessageSnapshotKey(messages = [], meta = {}) {
|
|
|
3798
4018
|
done: !!thought?.done,
|
|
3799
4019
|
timestamp: String(thought?.timestamp || '').trim(),
|
|
3800
4020
|
subagentRunId: String(thought?.subagentRunId || '').trim(),
|
|
4021
|
+
turnSeq: normalizeTurnSeq(thought?.turnSeq),
|
|
3801
4022
|
})),
|
|
4023
|
+
// The stream view orders by these: the count and the numbers.
|
|
4024
|
+
activityOrder: (Array.isArray(item?.activities) ? item.activities : [])
|
|
4025
|
+
.map((activity) => normalizeTurnSeq(activity?.turnSeq) ?? '-').join(','),
|
|
3802
4026
|
// Cheap signature only — runs are immutable once persisted, so runId +
|
|
3803
4027
|
// status is enough to catch a payload that differs only in its runs
|
|
3804
4028
|
// (without one, such a payload would short-circuit and the card would
|
|
@@ -3910,9 +4134,41 @@ export function renderNoConversationState() {
|
|
|
3910
4134
|
return true;
|
|
3911
4135
|
}
|
|
3912
4136
|
|
|
4137
|
+
// The last payload renderMessages (or a window merge) was given: the
|
|
4138
|
+
// fallback for a change of layout when no window is kept (the shared view).
|
|
4139
|
+
let lastRenderMessagesArgs = null;
|
|
4140
|
+
|
|
4141
|
+
/**
|
|
4142
|
+
* The stream view was switched: the transcript's assistant bubbles and the
|
|
4143
|
+
* live bubble take the other layout now. The messages are the kept window of
|
|
4144
|
+
* the conversation on screen (conversation-payload-cache.mjs), which the live
|
|
4145
|
+
* rows from the socket and every merge keep current; the last rendered
|
|
4146
|
+
* payload only when there is none.
|
|
4147
|
+
*/
|
|
4148
|
+
export function refreshStreamViewLayout() {
|
|
4149
|
+
const conversationId = String(currentConvId || '').trim();
|
|
4150
|
+
const kept = conversationId ? conversationPayloadCache.peekWindow(conversationId) : null;
|
|
4151
|
+
// In place: the reader keeps the text they are at.
|
|
4152
|
+
if (kept?.messages?.length) {
|
|
4153
|
+
const flags = { hasMoreOlder: kept.hasMoreOlder, hasMoreNewer: kept.hasMoreNewer };
|
|
4154
|
+
renderMessagesInPlace(kept.messages, { conversationId, ...flags, pageInfo: windowPageInfo(kept.messages, flags) });
|
|
4155
|
+
} else if (lastRenderMessagesArgs) {
|
|
4156
|
+
const { msgs, meta } = lastRenderMessagesArgs;
|
|
4157
|
+
renderMessagesInPlace(msgs, meta);
|
|
4158
|
+
}
|
|
4159
|
+
const messageId = thinkingMessageId;
|
|
4160
|
+
if (messageId && document.getElementById('thinking-indicator')) {
|
|
4161
|
+
takeThinkingBubbleOff();
|
|
4162
|
+
showThinking(messageId, false);
|
|
4163
|
+
renderThinkingActivities();
|
|
4164
|
+
renderRestoredSubagentBubbles(messageId);
|
|
4165
|
+
}
|
|
4166
|
+
}
|
|
4167
|
+
|
|
3913
4168
|
export function renderMessages(msgs, scroll = true, meta = {}) {
|
|
3914
4169
|
const el = getMessagesElement();
|
|
3915
4170
|
if (!el) return false;
|
|
4171
|
+
lastRenderMessagesArgs = { msgs, meta };
|
|
3916
4172
|
const ordered = sortConversationMessages(msgs || []);
|
|
3917
4173
|
const snapshotKey = buildMessageSnapshotKey(ordered, meta);
|
|
3918
4174
|
const statusViewMounted = !!el.querySelector('.status-view');
|
|
@@ -4437,6 +4693,7 @@ export function mergeMessagesIntoTranscript(previousMessages, msgs, meta = {}) {
|
|
|
4437
4693
|
renderRelayQuestions();
|
|
4438
4694
|
renderRelayBoards();
|
|
4439
4695
|
lastRenderedMessageSnapshotKey = snapshotKey;
|
|
4696
|
+
lastRenderMessagesArgs = { msgs, meta };
|
|
4440
4697
|
return true;
|
|
4441
4698
|
}
|
|
4442
4699
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// action never "jumps" a higher one back into the header.
|
|
13
13
|
|
|
14
14
|
export const HEADER_PRIORITY_ACTIONS = Object.freeze([
|
|
15
|
+
Object.freeze({ key: 'questions', inlineId: 'questions-btn', menuId: 'chat-menu-questions' }),
|
|
15
16
|
Object.freeze({ key: 'pins', inlineId: 'pinned-messages-btn', menuId: 'chat-menu-pins' }),
|
|
16
17
|
Object.freeze({ key: 'hidden', inlineId: 'hidden-messages-btn', menuId: 'chat-menu-hidden' }),
|
|
17
18
|
Object.freeze({ key: 'search', inlineId: 'message-search-btn', menuId: 'chat-menu-search' }),
|
|
@@ -140,6 +141,7 @@ export function resolveQuestionBannerMode({
|
|
|
140
141
|
*
|
|
141
142
|
* @param {object} input
|
|
142
143
|
* @param {boolean} [input.sharedMode] the shared read-only view: Day/Night only
|
|
144
|
+
* @param {number} [input.questionCount] pending questions (📓) of the open conversation
|
|
143
145
|
* @param {number} [input.pinCount] pins of the open conversation
|
|
144
146
|
* @param {number} [input.hiddenCount] messages hidden from shared viewers
|
|
145
147
|
* @param {boolean} [input.fullscreenAvailable] pwa-install.js's verdict (a phone, not installed)
|
|
@@ -147,14 +149,16 @@ export function resolveQuestionBannerMode({
|
|
|
147
149
|
*/
|
|
148
150
|
export function resolveHeaderActionAvailability({
|
|
149
151
|
sharedMode = false,
|
|
152
|
+
questionCount = 0,
|
|
150
153
|
pinCount = 0,
|
|
151
154
|
hiddenCount = 0,
|
|
152
155
|
fullscreenAvailable = false,
|
|
153
156
|
} = {}) {
|
|
154
157
|
if (sharedMode) {
|
|
155
|
-
return { pins: false, hidden: false, search: false, context: false, usage: false, theme: true, fullscreen: false };
|
|
158
|
+
return { questions: false, pins: false, hidden: false, search: false, context: false, usage: false, theme: true, fullscreen: false };
|
|
156
159
|
}
|
|
157
160
|
return {
|
|
161
|
+
questions: Number(questionCount) > 0,
|
|
158
162
|
pins: Number(pinCount) > 0,
|
|
159
163
|
hidden: Number(hiddenCount) > 0,
|
|
160
164
|
search: true,
|