@oar-sh/oar 0.9.20 → 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 +325 -0
- package/README.md +13 -13
- package/bin/oar.js +2 -2
- package/package.json +1 -1
- package/server/claude-cloud-worker/claude-cloud-session-process.mjs +231 -1
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +31 -2
- package/server/claude-cloud-worker/claude-cloud-worker-steering.mjs +34 -0
- package/server/claude-worker/claude-sdk-adapter.mjs +149 -4
- package/server/claude-worker/claude-session-process.mjs +16 -4
- package/server/claude-worker/claude-session-worker-main.mjs +18 -2
- package/server/copilot-worker/copilot-sdk-session-process.mjs +75 -1
- package/server/copilot-worker/copilot-sdk-session-worker.mjs +18 -2
- package/server/cursor-worker/cursor-session-worker.mjs +21 -2
- package/server/cursor-worker/cursor-worker-steering.mjs +20 -0
- package/server/db-schema.mjs +19 -0
- package/server/grok-worker/grok-background-tasks.mjs +10 -0
- package/server/grok-worker/grok-session-worker.mjs +22 -2
- package/server/grok-worker/grok-turn-runner.mjs +4 -0
- package/server/grok-worker/grok-worker-steering.mjs +23 -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/about-ui.js +1 -1
- package/server/public/app/about-view.mjs +19 -0
- package/server/public/app/action-confirmations.js +1 -1
- package/server/public/app/activity-replay-state.mjs +160 -42
- package/server/public/app/api-client.js +11 -2
- package/server/public/app/ask-user-view.js +24 -9
- package/server/public/app/attachments-view.js +1 -1
- package/server/public/app/back-guard.mjs +31 -5
- package/server/public/app/bootstrap.js +618 -72
- package/server/public/app/chat-actions-menu-state.mjs +141 -4
- package/server/public/app/chat-header-meta.mjs +27 -0
- package/server/public/app/claude-billing-toggle-ui.js +2 -1
- package/server/public/app/claude-cloud-conversation-ui.js +2 -2
- package/server/public/app/composer-settings-label.mjs +90 -0
- package/server/public/app/composer-settings-ui.js +442 -0
- package/server/public/app/context-menu.mjs +36 -5
- package/server/public/app/context-usage-view.mjs +68 -0
- package/server/public/app/conversation-deep-link.mjs +15 -11
- package/server/public/app/conversation-group-drag.mjs +12 -2
- package/server/public/app/conversation-group-order.mjs +9 -2
- package/server/public/app/conversation-group-state.mjs +47 -10
- package/server/public/app/conversation-origin-ui.js +17 -1
- package/server/public/app/conversation-view.js +309 -49
- package/server/public/app/cwd-menu-placement.mjs +18 -6
- package/server/public/app/cwd-picker.js +36 -6
- package/server/public/app/emoji-view.js +1 -1
- package/server/public/app/font-scaling.js +26 -0
- package/server/public/app/header-priority-actions.mjs +238 -0
- package/server/public/app/held-details.mjs +202 -0
- package/server/public/app/hidden-messages-view.mjs +4 -0
- package/server/public/app/inbox-ui.js +88 -81
- package/server/public/app/inbox-view.mjs +35 -19
- package/server/public/app/journal-view.js +328 -58
- package/server/public/app/live-activity-cap.mjs +103 -0
- package/server/public/app/phone-landscape-ui.js +225 -0
- package/server/public/app/phone-landscape.mjs +99 -0
- package/server/public/app/phone-typing-state.mjs +104 -0
- package/server/public/app/pinned-messages-view.mjs +5 -0
- package/server/public/app/push-settings.js +2 -2
- package/server/public/app/pwa-install.js +24 -3
- 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-event-subscriptions-ui.js +71 -22
- package/server/public/app/relay-event-view.mjs +38 -0
- package/server/public/app/relay-restart-ui.js +2 -2
- package/server/public/app/relay-schedule-view.mjs +32 -6
- package/server/public/app/relay-schedules-ui.js +69 -20
- package/server/public/app/relay-thoughts.mjs +15 -1
- package/server/public/app/remote-relay-origin-view.mjs +41 -23
- package/server/public/app/session-history-ui.js +251 -0
- package/server/public/app/session-history.mjs +599 -0
- package/server/public/app/settings-modal.js +67 -6
- package/server/public/app/settings-tabs.js +12 -0
- package/server/public/app/share-state.js +1 -1
- package/server/public/app/shared-conversations-settings.js +2 -2
- package/server/public/app/socket-handlers.js +47 -13
- package/server/public/app/status-view.mjs +18 -2
- package/server/public/app/store.js +87 -16
- 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 +35 -8
- package/server/public/app/tab-strip-fade.mjs +101 -0
- package/server/public/app/tap-actions.js +66 -0
- package/server/public/app/thoughts-preference.mjs +79 -0
- package/server/public/app/turn-stream-view.mjs +124 -0
- package/server/public/app/usage-limit-ui.js +63 -2
- package/server/public/app/viewer-history.mjs +13 -2
- package/server/public/index.html +2603 -1767
- package/server/relay-tools.md +1 -0
- package/server/repositories/message-repository.mjs +33 -0
- package/server/repositories/question-repository.mjs +51 -7
- package/server/repositories/relay-turn-rows.mjs +56 -0
- package/server/repositories/session-repository.mjs +19 -0
- package/server/routes/ask-user-routes.mjs +34 -5
- package/server/routes/claude-cloud-routes.mjs +19 -0
- package/server/routes/messages-routes.mjs +92 -15
- package/server/routes/relay-ask-routes.mjs +15 -5
- package/server/routes/sessions-routes.mjs +117 -12
- package/server/server-runtime.mjs +367 -170
- package/server/services/about-service.mjs +22 -53
- package/server/services/claude-billing-swap-service.mjs +7 -20
- package/server/services/claude-cloud-session-service.mjs +47 -5
- 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 +84 -18
- package/server/services/session-activity-tracker.mjs +1 -1
- package/server/services/session-history-refresh-service.mjs +4 -0
- package/server/services/session-launch-mutex-service.mjs +23 -0
- package/server/services/session-worker-registry-service.mjs +36 -0
- package/server/services/session-worker-supervisor-service.mjs +36 -0
- package/server/services/session-worker-websocket-service.mjs +183 -31
- package/server/services/ssh-tunnel-manager-service.mjs +7 -0
- package/server/services/worker-recycle-service.mjs +772 -0
- package/server/services/worker-replacement.mjs +197 -0
- package/shared/ask-user-bridge.mjs +9 -6
- package/shared/build-id.mjs +128 -0
- package/shared/relay-asks.mjs +10 -0
- package/shared/relay-events.mjs +1 -0
- package/shared/worker-runtime/heartbeat.mjs +31 -0
- package/shared/worker-runtime/simple-steering-state.mjs +38 -0
- package/shared/worker-runtime/worker-keepalive.mjs +108 -0
- package/shared/worker-runtime/worker-websocket-link.mjs +44 -10
- package/shared/worker-runtime/working-directory.mjs +1 -1
|
@@ -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,8 @@ 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';
|
|
165
|
+
import { noteConversationShown } from './session-history-ui.js';
|
|
158
166
|
|
|
159
167
|
const CONVERSATION_HISTORY_PAGE_SIZE = 20;
|
|
160
168
|
const HISTORY_LOAD_MORE_ID = 'history-load-more';
|
|
@@ -1612,9 +1620,10 @@ function createMessageNode(msg, msgId = null, force = false) {
|
|
|
1612
1620
|
const content = renderMarkdownPreview(msg.text || '', false);
|
|
1613
1621
|
const attachments = Array.isArray(msg.attachments) ? msg.attachments : [];
|
|
1614
1622
|
// capRelayActivityEntries, not slice: a boundary past the cap would take
|
|
1615
|
-
// 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.
|
|
1616
1625
|
const activities = Array.isArray(msg.activities)
|
|
1617
|
-
?
|
|
1626
|
+
? capFinishedActivityEntries(msg.activities.filter(Boolean))
|
|
1618
1627
|
: [];
|
|
1619
1628
|
// A compaction that happened during this turn is rendered as a full-width
|
|
1620
1629
|
// break row immediately above this message (syncTranscriptSeparators reads
|
|
@@ -1633,8 +1642,27 @@ function createMessageNode(msg, msgId = null, force = false) {
|
|
|
1633
1642
|
const subagentRuns = Array.isArray(msg.subagentRuns) ? msg.subagentRuns.filter(Boolean) : [];
|
|
1634
1643
|
const mainActivities = activities.filter((item) => !(normalizeRelayActivityEntry(item)?.subagentRunId));
|
|
1635
1644
|
const mainThoughts = thoughts.filter((t) => !t?.subagentRunId);
|
|
1636
|
-
|
|
1637
|
-
|
|
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;
|
|
1638
1666
|
// One row per sub-agent the turn spawned, as in the composer's task panel;
|
|
1639
1667
|
// its title opens the agent's whole record. The store's copy wins: status
|
|
1640
1668
|
// events keep it current after the bubble was built.
|
|
@@ -1719,7 +1747,7 @@ function createMessageNode(msg, msgId = null, force = false) {
|
|
|
1719
1747
|
: '';
|
|
1720
1748
|
|
|
1721
1749
|
div.innerHTML = `
|
|
1722
|
-
<div class="${bubbleClass}">${shareVisibilityActionHtml}${thoughtsHtml}${originBadgeHtml}${
|
|
1750
|
+
<div class="${bubbleClass}">${shareVisibilityActionHtml}${thoughtsHtml}${originBadgeHtml}${replyHtml}${relayErrorCtaHtml}${attachmentHtml}${activityHtml}${subagentHtml}${workflowRunsHtml}${previewCardsHtml}${userBubbleActionsHtml}${stoppedSteerActionsHtml}${workerLaunchActionsHtml}</div>
|
|
1723
1751
|
<div class="msg-label">${label}${modelTag}${reasoningTag}${modeTag}${autoTag}${continuationTag}${crossProviderTag}${usageTurnTag}${usageRemainingTag}${usageStaleTag} · ${fmtDate(msg.timestamp)}</div>`;
|
|
1724
1752
|
|
|
1725
1753
|
const bubble = div.querySelector('.msg-bubble');
|
|
@@ -1914,10 +1942,12 @@ export function renderActivityMarkup(activities, promotedBoundaryEntry = undefin
|
|
|
1914
1942
|
const progressHtml = progress.length
|
|
1915
1943
|
? `<div class="msg-activity-list">${progress.map((item) => `<div class="msg-activity-item">${escHtml(decorateActivityText(relayActivityEntryText(item)))}</div>`).join('')}</div>`
|
|
1916
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;
|
|
1917
1947
|
const toolsHtml = tools.length
|
|
1918
1948
|
? `
|
|
1919
1949
|
<details class="msg-activity">
|
|
1920
|
-
<summary>🔧 Tool activity (${
|
|
1950
|
+
<summary>🔧 Tool activity (${toolCount})</summary>
|
|
1921
1951
|
<div class="msg-activity-list">${tools.map((item) => `<div class="msg-activity-item">${escHtml(decorateActivityText(relayActivityEntryText(item)))}</div>`).join('')}</div>
|
|
1922
1952
|
</details>`
|
|
1923
1953
|
: '';
|
|
@@ -2011,6 +2041,7 @@ export function showThinking(messageId = null, autoScroll = true) {
|
|
|
2011
2041
|
<summary>💭 Thoughts</summary>
|
|
2012
2042
|
<div class="thinking-thoughts-list"></div>
|
|
2013
2043
|
</details>
|
|
2044
|
+
${isStreamViewEnabled() ? '<div id="thinking-stream-list" class="thinking-stream-list"></div>' : ''}
|
|
2014
2045
|
<div id="thinking-stream" class="thinking-stream" hidden></div>
|
|
2015
2046
|
<div class="thinking-compacting-note" hidden>Compacting the conversation…</div>
|
|
2016
2047
|
<div class="dots"><span></span><span></span><span></span></div>
|
|
@@ -2021,8 +2052,10 @@ export function showThinking(messageId = null, autoScroll = true) {
|
|
|
2021
2052
|
syncThinkingCompaction(div);
|
|
2022
2053
|
renderThinkingThoughts();
|
|
2023
2054
|
renderThinkingStream();
|
|
2024
|
-
// 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.
|
|
2025
2057
|
renderRelayBoards();
|
|
2058
|
+
renderQuestionStubs();
|
|
2026
2059
|
observeThinkingBubble(div);
|
|
2027
2060
|
if (getThinkingFollowMode()) applyThinkingFollow();
|
|
2028
2061
|
else if (autoScroll) scrollBottom();
|
|
@@ -2157,7 +2190,7 @@ export function collapseThinkingThoughts() {
|
|
|
2157
2190
|
if (!(panel instanceof HTMLDetailsElement)) return;
|
|
2158
2191
|
panel.open = false;
|
|
2159
2192
|
panel.querySelectorAll('.thinking-thought').forEach((row) => {
|
|
2160
|
-
if (row instanceof HTMLDetailsElement) row
|
|
2193
|
+
if (row instanceof HTMLDetailsElement) showLiveThoughtRow(row, false);
|
|
2161
2194
|
});
|
|
2162
2195
|
}
|
|
2163
2196
|
|
|
@@ -2251,6 +2284,7 @@ export function renderThinkingActivities() {
|
|
|
2251
2284
|
// clear-and-replay (or the append path's last-row-only dedupe) would either
|
|
2252
2285
|
// wipe selections or duplicate rows on every changed payload.
|
|
2253
2286
|
const mainExpected = [];
|
|
2287
|
+
const mainEntries = [];
|
|
2254
2288
|
const bySubagentRun = new Map();
|
|
2255
2289
|
for (const item of items) {
|
|
2256
2290
|
const entry = normalizeRelayActivityEntry(item);
|
|
@@ -2260,15 +2294,22 @@ export function renderThinkingActivities() {
|
|
|
2260
2294
|
// would otherwise flip presentation the moment the turn lands.
|
|
2261
2295
|
if (isCompactBoundaryActivityEntry(entry)) continue;
|
|
2262
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;
|
|
2263
2300
|
if (entry.subagentRunId) {
|
|
2264
2301
|
const list = bySubagentRun.get(entry.subagentRunId) || [];
|
|
2265
|
-
if (list[list.length - 1] !== decorated) list.push(decorated);
|
|
2302
|
+
if (numbered || list[list.length - 1] !== decorated) list.push(decorated);
|
|
2266
2303
|
bySubagentRun.set(entry.subagentRunId, list);
|
|
2267
|
-
} else if (mainExpected[mainExpected.length - 1] !== decorated) {
|
|
2304
|
+
} else if (numbered || mainExpected[mainExpected.length - 1] !== decorated) {
|
|
2268
2305
|
mainExpected.push(decorated);
|
|
2306
|
+
mainEntries.push({ text: decorated, turnSeq: entry.turnSeq ?? null });
|
|
2269
2307
|
}
|
|
2270
2308
|
}
|
|
2271
|
-
|
|
2309
|
+
const streamList = liveStreamList();
|
|
2310
|
+
if (streamList) {
|
|
2311
|
+
if (!selectionIntersectsNode(streamList)) patchStreamListActivities(streamList, mainEntries);
|
|
2312
|
+
} else if (!selectionIntersectsNode(box)) {
|
|
2272
2313
|
patchActivityList(box, mainExpected, 'thinking-activity-item');
|
|
2273
2314
|
}
|
|
2274
2315
|
syncThinkingCompaction();
|
|
@@ -2296,7 +2337,9 @@ export function restoreInFlightThinking(inFlight, autoScroll = true) {
|
|
|
2296
2337
|
// live poll would otherwise churn the DOM (and the user's selection) with
|
|
2297
2338
|
// identical content. The indicator must belong to this turn's message —
|
|
2298
2339
|
// skipping on a foreign bubble would let a misplaced one stick around.
|
|
2299
|
-
|
|
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)}`;
|
|
2300
2343
|
const liveIndicator = document.getElementById('thinking-indicator');
|
|
2301
2344
|
if (snapshotKey === lastInFlightSnapshotKey
|
|
2302
2345
|
&& liveIndicator
|
|
@@ -2312,6 +2355,7 @@ export function restoreInFlightThinking(inFlight, autoScroll = true) {
|
|
|
2312
2355
|
const activities = mergeRelayActivityTexts(
|
|
2313
2356
|
relayActivities.get(messageId) || [],
|
|
2314
2357
|
Array.isArray(inFlight.activities) ? inFlight.activities : [],
|
|
2358
|
+
liveActivityCap(),
|
|
2315
2359
|
);
|
|
2316
2360
|
relayActivities.set(messageId, activities);
|
|
2317
2361
|
const inFlightThoughts = Array.isArray(inFlight.thoughts) ? inFlight.thoughts : [];
|
|
@@ -2319,7 +2363,15 @@ export function restoreInFlightThinking(inFlight, autoScroll = true) {
|
|
|
2319
2363
|
const thoughtMap = relayThoughts.get(messageId) || new Map();
|
|
2320
2364
|
for (const entry of inFlightThoughts) {
|
|
2321
2365
|
const key = String(entry?.reasoningId || `seq-${entry?.seq || thoughtMap.size}`);
|
|
2322
|
-
|
|
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
|
+
});
|
|
2323
2375
|
}
|
|
2324
2376
|
relayThoughts.set(messageId, thoughtMap);
|
|
2325
2377
|
}
|
|
@@ -2404,6 +2456,7 @@ export function appendThinkingActivity(item, subagentRunId = null, autoScroll =
|
|
|
2404
2456
|
row.className = 'subagent-activity-item';
|
|
2405
2457
|
row.textContent = decorated;
|
|
2406
2458
|
activityBox.appendChild(row);
|
|
2459
|
+
trimLiveActivityRows(activityBox, 'subagent-activity-item');
|
|
2407
2460
|
refreshSubagentSummary(subagentBubble);
|
|
2408
2461
|
}
|
|
2409
2462
|
}
|
|
@@ -2414,18 +2467,105 @@ export function appendThinkingActivity(item, subagentRunId = null, autoScroll =
|
|
|
2414
2467
|
return;
|
|
2415
2468
|
}
|
|
2416
2469
|
|
|
2417
|
-
|
|
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');
|
|
2418
2474
|
if (!box) return;
|
|
2419
|
-
const
|
|
2420
|
-
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
|
+
}
|
|
2421
2485
|
const row = document.createElement('div');
|
|
2422
2486
|
row.className = 'thinking-activity-item';
|
|
2423
|
-
if (
|
|
2487
|
+
if (numbered) row.dataset.turnSeq = String(entry.turnSeq);
|
|
2424
2488
|
row.textContent = decorated;
|
|
2425
|
-
|
|
2489
|
+
if (streamList) placeByTurnSeq(streamList, row);
|
|
2490
|
+
else box.appendChild(row);
|
|
2491
|
+
trimLiveActivityRows(box, 'thinking-activity-item');
|
|
2426
2492
|
if (autoScroll) scrollBottom();
|
|
2427
2493
|
}
|
|
2428
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
|
+
|
|
2429
2569
|
function getSubagentDisplayName(subagentRunId) {
|
|
2430
2570
|
const entry = typeof getSubagentRun === 'function' ? getSubagentRun(subagentRunId) : null;
|
|
2431
2571
|
if (entry?.displayName) return entry.displayName;
|
|
@@ -2567,7 +2707,11 @@ function ensureSubagentBubble(subagentRunId, depth = 0) {
|
|
|
2567
2707
|
}
|
|
2568
2708
|
}
|
|
2569
2709
|
if (entry?.activities?.length) {
|
|
2570
|
-
|
|
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) {
|
|
2571
2715
|
const text = typeof item === 'string' ? item : String(item?.text || '').trim();
|
|
2572
2716
|
if (!text) continue;
|
|
2573
2717
|
const decorated = decorateActivityText(text);
|
|
@@ -2594,6 +2738,7 @@ function refreshSubagentSummary(bubble) {
|
|
|
2594
2738
|
const text = subagentFoldSummary({
|
|
2595
2739
|
activities: Array.from(bubble.querySelectorAll(':scope > .subagent-activity > .subagent-activity-item'), (row) => row.textContent || ''),
|
|
2596
2740
|
streamText: bubble.querySelector(':scope > .subagent-stream')?.textContent || '',
|
|
2741
|
+
cap: liveActivityCap(),
|
|
2597
2742
|
});
|
|
2598
2743
|
if (summary.textContent !== text) summary.textContent = text;
|
|
2599
2744
|
}
|
|
@@ -2716,17 +2861,95 @@ function thoughtSummaryText(text) {
|
|
|
2716
2861
|
return value.length > 80 ? `${value.slice(0, 80)}…` : value;
|
|
2717
2862
|
}
|
|
2718
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
|
+
*/
|
|
2719
2869
|
export function setLiveThinkingThoughtState(row, done = false) {
|
|
2720
2870
|
if (!row) return;
|
|
2721
2871
|
row.dataset.done = done ? '1' : '0';
|
|
2722
|
-
// The temporary panel represents an active turn, so every thought remains visible
|
|
2723
|
-
// until the completed assistant message replaces it with collapsed history.
|
|
2724
|
-
row.open = true;
|
|
2725
2872
|
}
|
|
2726
2873
|
|
|
2727
|
-
|
|
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) {
|
|
2728
2950
|
const key = String(reasoningId || 'reasoning');
|
|
2729
2951
|
const value = String(text || '');
|
|
2952
|
+
const turnId = liveThoughtTurnId(messageId, subagentRunId);
|
|
2730
2953
|
|
|
2731
2954
|
if (subagentRunId) {
|
|
2732
2955
|
const subagentBubble = ensureSubagentBubble(subagentRunId);
|
|
@@ -2741,16 +2964,7 @@ export function appendThinkingThought(reasoningId, text, done = false, subagentR
|
|
|
2741
2964
|
}
|
|
2742
2965
|
let row = thoughtsBox.querySelector(`.thinking-thought[data-reasoning-id="${CSS.escape(key)}"]`);
|
|
2743
2966
|
if (!row) {
|
|
2744
|
-
row =
|
|
2745
|
-
row.className = 'thinking-thought';
|
|
2746
|
-
row.open = !done;
|
|
2747
|
-
row.dataset.reasoningId = key;
|
|
2748
|
-
row.dataset.subagentRunId = subagentRunId;
|
|
2749
|
-
const summary = document.createElement('summary');
|
|
2750
|
-
const body = document.createElement('div');
|
|
2751
|
-
body.className = 'thinking-thought-body';
|
|
2752
|
-
row.appendChild(summary);
|
|
2753
|
-
row.appendChild(body);
|
|
2967
|
+
row = createLiveThoughtRow(key, subagentRunId, turnId);
|
|
2754
2968
|
thoughtsBox.appendChild(row);
|
|
2755
2969
|
}
|
|
2756
2970
|
const summaryEl = row.querySelector('summary');
|
|
@@ -2765,22 +2979,23 @@ export function appendThinkingThought(reasoningId, text, done = false, subagentR
|
|
|
2765
2979
|
return;
|
|
2766
2980
|
}
|
|
2767
2981
|
|
|
2768
|
-
|
|
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');
|
|
2769
2986
|
if (!box) return;
|
|
2770
2987
|
let row = box.querySelector(`.thinking-thought[data-reasoning-id="${CSS.escape(key)}"]`);
|
|
2771
2988
|
if (!row) {
|
|
2772
|
-
row =
|
|
2773
|
-
row.className = 'thinking-thought';
|
|
2774
|
-
row.open = !done;
|
|
2775
|
-
row.dataset.reasoningId = key;
|
|
2776
|
-
if (subagentRunId) row.dataset.subagentRunId = subagentRunId;
|
|
2777
|
-
const summary = document.createElement('summary');
|
|
2778
|
-
const body = document.createElement('div');
|
|
2779
|
-
body.className = 'thinking-thought-body';
|
|
2780
|
-
row.appendChild(summary);
|
|
2781
|
-
row.appendChild(body);
|
|
2989
|
+
row = createLiveThoughtRow(key, subagentRunId, turnId);
|
|
2782
2990
|
box.appendChild(row);
|
|
2783
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
|
+
}
|
|
2784
2999
|
const summaryEl = row.querySelector('summary');
|
|
2785
3000
|
const bodyEl = row.querySelector('.thinking-thought-body');
|
|
2786
3001
|
if (summaryEl) summaryEl.textContent = `💭 ${thoughtSummaryText(value)}`;
|
|
@@ -2795,7 +3010,7 @@ export function renderThinkingThoughts() {
|
|
|
2795
3010
|
const thoughtMap = thinkingMessageId ? relayThoughts.get(thinkingMessageId) : null;
|
|
2796
3011
|
if (!thoughtMap || !thoughtMap.size) return;
|
|
2797
3012
|
for (const entry of thoughtMap.values()) {
|
|
2798
|
-
appendThinkingThought(entry.reasoningId, entry.text, entry.done, entry.subagentRunId || null, false);
|
|
3013
|
+
appendThinkingThought(entry.reasoningId, entry.text, entry.done, entry.subagentRunId || null, false, thinkingMessageId);
|
|
2799
3014
|
}
|
|
2800
3015
|
}
|
|
2801
3016
|
|
|
@@ -3735,8 +3950,12 @@ export function appendMessage(msg, scroll = true, msgId = null, force = false, i
|
|
|
3735
3950
|
}
|
|
3736
3951
|
}
|
|
3737
3952
|
if (isNewNode && insertedNode) {
|
|
3738
|
-
// A reply takes over the board its turn posted (relay-board-view)
|
|
3739
|
-
|
|
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
|
+
}
|
|
3740
3959
|
syncSeparatorsNow();
|
|
3741
3960
|
if (scroll) scrollBottom();
|
|
3742
3961
|
}
|
|
@@ -3770,6 +3989,8 @@ function buildMessageSnapshotKey(messages = [], meta = {}) {
|
|
|
3770
3989
|
conversationId,
|
|
3771
3990
|
hasMoreOlder: !!hasMoreOlder,
|
|
3772
3991
|
hasMoreNewer: !!hasMoreNewer,
|
|
3992
|
+
// The stream view lays the same payload out differently.
|
|
3993
|
+
streamView: isStreamViewEnabled(),
|
|
3773
3994
|
messages: (Array.isArray(messages) ? messages : []).map((item) => ({
|
|
3774
3995
|
id: String(item?.id || '').trim(),
|
|
3775
3996
|
role: String(item?.role || '').trim(),
|
|
@@ -3797,7 +4018,11 @@ function buildMessageSnapshotKey(messages = [], meta = {}) {
|
|
|
3797
4018
|
done: !!thought?.done,
|
|
3798
4019
|
timestamp: String(thought?.timestamp || '').trim(),
|
|
3799
4020
|
subagentRunId: String(thought?.subagentRunId || '').trim(),
|
|
4021
|
+
turnSeq: normalizeTurnSeq(thought?.turnSeq),
|
|
3800
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(','),
|
|
3801
4026
|
// Cheap signature only — runs are immutable once persisted, so runId +
|
|
3802
4027
|
// status is enough to catch a payload that differs only in its runs
|
|
3803
4028
|
// (without one, such a payload would short-circuit and the card would
|
|
@@ -3909,9 +4134,41 @@ export function renderNoConversationState() {
|
|
|
3909
4134
|
return true;
|
|
3910
4135
|
}
|
|
3911
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
|
+
|
|
3912
4168
|
export function renderMessages(msgs, scroll = true, meta = {}) {
|
|
3913
4169
|
const el = getMessagesElement();
|
|
3914
4170
|
if (!el) return false;
|
|
4171
|
+
lastRenderMessagesArgs = { msgs, meta };
|
|
3915
4172
|
const ordered = sortConversationMessages(msgs || []);
|
|
3916
4173
|
const snapshotKey = buildMessageSnapshotKey(ordered, meta);
|
|
3917
4174
|
const statusViewMounted = !!el.querySelector('.status-view');
|
|
@@ -4436,6 +4693,7 @@ export function mergeMessagesIntoTranscript(previousMessages, msgs, meta = {}) {
|
|
|
4436
4693
|
renderRelayQuestions();
|
|
4437
4694
|
renderRelayBoards();
|
|
4438
4695
|
lastRenderedMessageSnapshotKey = snapshotKey;
|
|
4696
|
+
lastRenderMessagesArgs = { msgs, meta };
|
|
4439
4697
|
return true;
|
|
4440
4698
|
}
|
|
4441
4699
|
|
|
@@ -4755,6 +5013,8 @@ export async function sendMessage() {
|
|
|
4755
5013
|
const adoptNewConversation = viewingSendConversation();
|
|
4756
5014
|
if (adoptNewConversation) {
|
|
4757
5015
|
setCurrentConv(r.conversationId);
|
|
5016
|
+
// The new chat gets its history entry, as one opened from the list does.
|
|
5017
|
+
noteConversationShown(r.conversationId);
|
|
4758
5018
|
composerConversationId = r.conversationId;
|
|
4759
5019
|
nameOwnSendConversation(send, r.conversationId);
|
|
4760
5020
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Kept DOM-free so they can run under `node --test` without a browser.
|
|
3
3
|
|
|
4
4
|
const FLIP_THRESHOLD_PX = 180;
|
|
5
|
+
const FALLBACK_WIDTH_PX = 260;
|
|
5
6
|
|
|
6
7
|
function clamp(value, min, max) {
|
|
7
8
|
if (max < min) return min;
|
|
@@ -11,17 +12,28 @@ function clamp(value, min, max) {
|
|
|
11
12
|
/**
|
|
12
13
|
* Decide where the fixed-position picker panel should sit relative to its trigger.
|
|
13
14
|
* Used on desktop only; the mobile layout is a bottom sheet driven purely by CSS.
|
|
15
|
+
*
|
|
16
|
+
* `gap`, `edge`, `minHeight`, `maxHeight` are design sizes at 100 %; `scale`
|
|
17
|
+
* is the UI zoom (font-scaling.js getUiScale) they are multiplied by, so the
|
|
18
|
+
* panel keeps its proportions next to the scaled stylesheet. The trigger rect
|
|
19
|
+
* and the viewport are measured, never scaled.
|
|
14
20
|
*/
|
|
15
21
|
export function resolveCwdMenuPlacement({
|
|
16
22
|
triggerRect,
|
|
17
23
|
viewportWidth = 0,
|
|
18
24
|
viewportHeight = 0,
|
|
19
25
|
panelHeight = 0,
|
|
20
|
-
gap = 6,
|
|
21
|
-
edge = 8,
|
|
22
|
-
minHeight = 140,
|
|
23
|
-
maxHeight = 300,
|
|
26
|
+
gap: gapBase = 6,
|
|
27
|
+
edge: edgeBase = 8,
|
|
28
|
+
minHeight: minHeightBase = 140,
|
|
29
|
+
maxHeight: maxHeightBase = 300,
|
|
30
|
+
scale = 1,
|
|
24
31
|
} = {}) {
|
|
32
|
+
const factor = Number.isFinite(Number(scale)) && Number(scale) > 0 ? Number(scale) : 1;
|
|
33
|
+
const gap = gapBase * factor;
|
|
34
|
+
const edge = edgeBase * factor;
|
|
35
|
+
const minHeight = minHeightBase * factor;
|
|
36
|
+
const maxHeight = maxHeightBase * factor;
|
|
25
37
|
const top = Number(triggerRect?.top) || 0;
|
|
26
38
|
const left = Number(triggerRect?.left) || 0;
|
|
27
39
|
const width = Math.max(0, Number(triggerRect?.width) || 0);
|
|
@@ -30,13 +42,13 @@ export function resolveCwdMenuPlacement({
|
|
|
30
42
|
|
|
31
43
|
const spaceBelow = Math.max(0, viewportHeight - bottom - gap - edge);
|
|
32
44
|
const spaceAbove = Math.max(0, top - gap - edge);
|
|
33
|
-
const flip = spaceBelow < FLIP_THRESHOLD_PX && spaceAbove > spaceBelow;
|
|
45
|
+
const flip = spaceBelow < FLIP_THRESHOLD_PX * factor && spaceAbove > spaceBelow;
|
|
34
46
|
const available = flip ? spaceAbove : spaceBelow;
|
|
35
47
|
|
|
36
48
|
const desiredHeight = panelHeight > 0 ? Math.min(panelHeight, maxHeight) : maxHeight;
|
|
37
49
|
const resolvedMaxHeight = clamp(Math.min(desiredHeight, available), Math.min(minHeight, available), maxHeight);
|
|
38
50
|
|
|
39
|
-
const resolvedWidth = width > 0 ? width :
|
|
51
|
+
const resolvedWidth = width > 0 ? width : FALLBACK_WIDTH_PX * factor;
|
|
40
52
|
const maxLeft = Math.max(edge, viewportWidth - resolvedWidth - edge);
|
|
41
53
|
const resolvedLeft = clamp(left, edge, maxLeft);
|
|
42
54
|
const resolvedTop = flip
|