@oar-sh/oar 0.9.21 โ 0.9.23
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 +206 -0
- package/README.md +9 -9
- package/bin/oar.js +28 -0
- package/package.json +13 -14
- package/server/claude-cloud-worker/claude-cloud-session-worker.mjs +9 -1
- package/server/claude-worker/claude-cli-pinned-env.mjs +4 -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 +46 -34
- package/server/routes/relay-ask-routes.mjs +15 -5
- package/server/routes/sessions-routes.mjs +77 -6
- package/server/server-runtime.mjs +199 -171
- package/server/services/cloudflared-tunnel-service.mjs +7 -0
- package/server/services/earlier-database-service.mjs +1 -1
- package/server/services/file-serving.mjs +13 -0
- package/server/services/idle-timers.mjs +184 -0
- package/server/services/native-addon-build-service.mjs +224 -0
- package/server/services/oar-cli-helpers.mjs +7 -4
- 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-console-log.mjs +2 -3
- package/server/services/relay-event-service.mjs +4 -1
- package/server/services/remote-relay-dispatcher.mjs +23 -4
- package/server/services/remote-relay-loopback.mjs +3 -3
- package/server/services/sdk-session-sync-service.mjs +1 -1
- 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/server/services/update-install-service.mjs +32 -1
- 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
- package/server/services/package-data-rescue-preinstall.mjs +0 -22
- package/server/services/package-data-rescue.mjs +0 -139
- package/server/tty-console-bootstrap.mjs +0 -75
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
// A row's place in its turn's order (migration 0038), or null: rows from
|
|
2
|
+
// before it, or from an older relay, have none.
|
|
3
|
+
export function normalizeTurnSeq(value) {
|
|
4
|
+
if (value === null || value === undefined || value === '') return null;
|
|
5
|
+
const num = Number(value);
|
|
6
|
+
return Number.isInteger(num) && num > 0 ? num : null;
|
|
7
|
+
}
|
|
2
8
|
|
|
3
9
|
export function normalizeRelayActivityEntry(item) {
|
|
4
10
|
if (item && typeof item === 'object' && !Array.isArray(item)) {
|
|
@@ -8,7 +14,8 @@ export function normalizeRelayActivityEntry(item) {
|
|
|
8
14
|
const metadata = (item.metadata && typeof item.metadata === 'object' && !Array.isArray(item.metadata))
|
|
9
15
|
? item.metadata
|
|
10
16
|
: null;
|
|
11
|
-
|
|
17
|
+
const turnSeq = normalizeTurnSeq(item.turnSeq);
|
|
18
|
+
return { text, subagentRunId, metadata, ...(turnSeq !== null ? { turnSeq } : {}) };
|
|
12
19
|
}
|
|
13
20
|
const text = String(item || '').trim();
|
|
14
21
|
if (!text) return null;
|
|
@@ -24,25 +31,87 @@ export function isCompactBoundaryActivityEntry(item) {
|
|
|
24
31
|
return normalizeRelayActivityEntry(item)?.metadata?.kind === 'compact_boundary';
|
|
25
32
|
}
|
|
26
33
|
|
|
27
|
-
// The
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
// The thread an activity row belongs to: a sub-agent run, or '' for the main
|
|
35
|
+
// agent. Every cap below counts each thread apart, so a busy sub-agent never
|
|
36
|
+
// pushes the main agent's lines out (or another sub-agent's).
|
|
37
|
+
function activityThreadKey(item) {
|
|
38
|
+
return normalizeRelayActivityEntry(item)?.subagentRunId || '';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// No limit: a cap of 0 (the setting's far end), or anything not a positive
|
|
42
|
+
// number.
|
|
43
|
+
function isUnlimitedCap(cap) {
|
|
44
|
+
const value = Number(cap);
|
|
45
|
+
return !Number.isFinite(value) || value <= 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Keep the newest `cap` rows of each thread among those `isKept` does not
|
|
50
|
+
* protect, and every protected row; the order stays. A sub-agent's thread
|
|
51
|
+
* keeps `subagentCap` (the same unless given). A list already within the cap
|
|
52
|
+
* is returned as it is.
|
|
53
|
+
*/
|
|
54
|
+
function capNewestPerThread(list, cap, isKept, subagentCap = cap) {
|
|
55
|
+
if (isUnlimitedCap(cap) && isUnlimitedCap(subagentCap)) return list;
|
|
56
|
+
const maxFor = (key) => {
|
|
57
|
+
const limit = key ? subagentCap : cap;
|
|
58
|
+
return isUnlimitedCap(limit) ? Number.POSITIVE_INFINITY : Math.trunc(Number(limit));
|
|
59
|
+
};
|
|
60
|
+
const remaining = new Map();
|
|
61
|
+
for (const item of list) {
|
|
62
|
+
if (isKept(item)) continue;
|
|
63
|
+
const key = activityThreadKey(item);
|
|
64
|
+
remaining.set(key, (remaining.get(key) || 0) + 1);
|
|
65
|
+
}
|
|
66
|
+
if ([...remaining.entries()].every(([key, count]) => count <= maxFor(key))) return list;
|
|
67
|
+
return list.filter((item) => {
|
|
68
|
+
if (isKept(item)) return true;
|
|
69
|
+
const key = activityThreadKey(item);
|
|
70
|
+
const left = remaining.get(key) - 1;
|
|
71
|
+
remaining.set(key, left);
|
|
72
|
+
return left < maxFor(key);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The live list of one turn keeps the newest ordinary lines of each thread
|
|
77
|
+
// (the main agent, each sub-agent) only, as many as the "Tool lines shown
|
|
78
|
+
// while an agent works" setting says (live-activity-cap.mjs; 0 keeps all),
|
|
79
|
+
// but every structured row a line of the transcript is built from: the
|
|
80
|
+
// turn's compaction entries and the session-restart notes (a compaction
|
|
81
|
+
// window, a billing switch) would otherwise scroll out of a busy turn and
|
|
82
|
+
// take their line with them until the next reload. The call sites pass the
|
|
83
|
+
// setting; LIVE_ACTIVITY_CAP is its default.
|
|
84
|
+
export const LIVE_ACTIVITY_CAP = 50;
|
|
33
85
|
function isKeptLiveActivityEntry(item) {
|
|
34
86
|
const kind = normalizeRelayActivityEntry(item)?.metadata?.kind;
|
|
35
87
|
return kind === 'compact_boundary' || kind === 'compact_window_respawn' || kind === 'billing_switch_respawn';
|
|
36
88
|
}
|
|
37
89
|
export function capLiveActivityEntries(items, cap = LIVE_ACTIVITY_CAP) {
|
|
38
90
|
const list = Array.isArray(items) ? items : [];
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
91
|
+
return capNewestPerThread(list, cap, isKeptLiveActivityEntry);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// What the relay sends of a turn's activity rows, per thread.
|
|
95
|
+
//
|
|
96
|
+
// A running turn (the in-flight snapshot the live bubble polls every 900 ms):
|
|
97
|
+
// as many as the browser shows, which it says with `activityCap` (its "Tool
|
|
98
|
+
// lines shown" setting); a request without it (an older page, a paired
|
|
99
|
+
// relay's read) gets the default 50; the setting's "no limit" asks for the
|
|
100
|
+
// most the relay sends, 500.
|
|
101
|
+
export const IN_FLIGHT_ACTIVITY_LIMIT_PER_THREAD = 500;
|
|
102
|
+
// A finished one (the message payload): the main agent's newest 200 (the
|
|
103
|
+
// stream view lists them), each sub-agent's newest 50 (its own record is in
|
|
104
|
+
// the sub-agent viewer).
|
|
105
|
+
export const FINISHED_ACTIVITY_LIMIT_PER_THREAD = 200;
|
|
106
|
+
export const FINISHED_SUBAGENT_ACTIVITY_LIMIT_PER_THREAD = 50;
|
|
107
|
+
|
|
108
|
+
/** The in-flight cap a request asks for: `activityCap` as 1โฆ500, 0 โ 500, else 50. */
|
|
109
|
+
export function inFlightActivityCap(value) {
|
|
110
|
+
const text = String(value ?? '').trim();
|
|
111
|
+
if (!/^\d+$/.test(text)) return LIVE_ACTIVITY_CAP;
|
|
112
|
+
const cap = Number(text);
|
|
113
|
+
if (cap === 0) return IN_FLIGHT_ACTIVITY_LIMIT_PER_THREAD;
|
|
114
|
+
return Math.min(IN_FLIGHT_ACTIVITY_LIMIT_PER_THREAD, cap);
|
|
46
115
|
}
|
|
47
116
|
|
|
48
117
|
// A compaction is published in up to two steps on the same message: a
|
|
@@ -118,56 +187,99 @@ export function visibleActivityEntries(items, promoted) {
|
|
|
118
187
|
});
|
|
119
188
|
}
|
|
120
189
|
|
|
121
|
-
//
|
|
122
|
-
//
|
|
190
|
+
// The relay's cap of a turn's activity rows, which never drops structured
|
|
191
|
+
// rows. Each thread (the main agent, each sub-agent) keeps its newest `limit`
|
|
192
|
+
// prose rows: a busy sub-agent no longer pushes the main agent's lines out,
|
|
193
|
+
// and the latest lines are the ones a reader of a long turn is after. A
|
|
123
194
|
// metadata-bearing row carries transcript structure rather than prose, and a
|
|
124
195
|
// long agentic turn can push its compaction boundary past the cap โ losing
|
|
125
196
|
// the break row entirely, with no prose fallback since the bubble filters
|
|
126
|
-
// compact entries out. Keep every structured row,
|
|
127
|
-
//
|
|
128
|
-
|
|
197
|
+
// compact entries out. Keep every structured row, preserve the original
|
|
198
|
+
// order. Returns a new array. `subagentLimit` is a sub-agent thread's own
|
|
199
|
+
// limit (the finished payload keeps fewer of those).
|
|
200
|
+
//
|
|
201
|
+
// `omittedMarker`: when the main agent's rows were cut, a structured row
|
|
202
|
+
// saying how many (kind ACTIVITY_OMITTED_KIND) goes right before its first
|
|
203
|
+
// kept row, so a reader of the finished turn sees that its list starts late.
|
|
204
|
+
export function capRelayActivityEntries(items, limit, { subagentLimit = limit, omittedMarker = false } = {}) {
|
|
129
205
|
const list = Array.isArray(items) ? items : [];
|
|
130
|
-
const max = Math.max(1, Math.trunc(Number(limit)) ||
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
if (normalizeRelayActivityEntry(list[index])?.metadata) structuredIndexes.push(index);
|
|
135
|
-
}
|
|
136
|
-
if (!structuredIndexes.length) return list.slice(0, max);
|
|
206
|
+
const max = Math.max(1, Math.trunc(Number(limit)) || LIVE_ACTIVITY_CAP);
|
|
207
|
+
const subagentMax = Math.max(1, Math.trunc(Number(subagentLimit)) || max);
|
|
208
|
+
const isStructured = (item) => !!normalizeRelayActivityEntry(item)?.metadata;
|
|
209
|
+
const structured = list.filter(isStructured);
|
|
137
210
|
// Safety valve for a pathological turn with more structured rows than the
|
|
138
211
|
// cap allows: the most recent ones win.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (keep.has(index)) {
|
|
144
|
-
kept.push(list[index]);
|
|
145
|
-
continue;
|
|
146
|
-
}
|
|
147
|
-
if (budget <= 0) continue;
|
|
148
|
-
budget -= 1;
|
|
149
|
-
kept.push(list[index]);
|
|
212
|
+
let source = list;
|
|
213
|
+
if (structured.length > max) {
|
|
214
|
+
const keep = new Set(structured.slice(-max));
|
|
215
|
+
source = list.filter((item) => !isStructured(item) || keep.has(item));
|
|
150
216
|
}
|
|
151
|
-
|
|
217
|
+
const capped = capNewestPerThread(source, max, isStructured, subagentMax);
|
|
218
|
+
if (capped === list) return list.slice();
|
|
219
|
+
if (!omittedMarker) return capped;
|
|
220
|
+
const isMainProse = (item) => !isStructured(item) && !activityThreadKey(item);
|
|
221
|
+
const kept = new Set(capped);
|
|
222
|
+
const dropped = source.filter((item) => isMainProse(item) && !kept.has(item));
|
|
223
|
+
if (!dropped.length) return capped;
|
|
224
|
+
const at = capped.findIndex(isMainProse);
|
|
225
|
+
// Where the first line left out was in the turn's order, when it had one.
|
|
226
|
+
const fromTurnSeq = normalizeRelayActivityEntry(dropped[0])?.turnSeq ?? null;
|
|
227
|
+
const marker = omittedActivityMarker(dropped.length, fromTurnSeq);
|
|
228
|
+
return at === -1 ? [...capped, marker] : [...capped.slice(0, at), marker, ...capped.slice(at)];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// The row that stands for the main agent's tool lines a finished turn's cap
|
|
232
|
+
// left out. It has no turn order of its own; `fromTurnSeq` is the place of
|
|
233
|
+
// the first line it stands for, where the stream view puts it.
|
|
234
|
+
export const ACTIVITY_OMITTED_KIND = 'activity_omitted';
|
|
235
|
+
|
|
236
|
+
export function omittedActivityMarker(count, fromTurnSeq = null) {
|
|
237
|
+
const n = Math.max(1, Math.trunc(Number(count)) || 1);
|
|
238
|
+
const from = normalizeTurnSeq(fromTurnSeq);
|
|
239
|
+
return {
|
|
240
|
+
text: `โฆ ${n} earlier tool ${n === 1 ? 'line' : 'lines'} omitted`,
|
|
241
|
+
subagentRunId: null,
|
|
242
|
+
metadata: { kind: ACTIVITY_OMITTED_KIND, count: n, ...(from !== null ? { fromTurnSeq: from } : {}) },
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function isOmittedActivityMarker(item) {
|
|
247
|
+
return normalizeRelayActivityEntry(item)?.metadata?.kind === ACTIVITY_OMITTED_KIND;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// The finished turn's cap, as the relay sends it and the bubble reads it; a
|
|
251
|
+
// cut in the main agent's lines is marked.
|
|
252
|
+
export function capFinishedActivityEntries(items) {
|
|
253
|
+
return capRelayActivityEntries(items, FINISHED_ACTIVITY_LIMIT_PER_THREAD, {
|
|
254
|
+
subagentLimit: FINISHED_SUBAGENT_ACTIVITY_LIMIT_PER_THREAD,
|
|
255
|
+
omittedMarker: true,
|
|
256
|
+
});
|
|
152
257
|
}
|
|
153
258
|
|
|
154
259
|
export function relayActivityEntryText(item) {
|
|
155
260
|
return normalizeRelayActivityEntry(item)?.text || '';
|
|
156
261
|
}
|
|
157
262
|
|
|
158
|
-
|
|
263
|
+
// The live list's rule: the newest `cap` ordinary lines of each thread, the
|
|
264
|
+
// structured rows kept (capLiveActivityEntries); Infinity or 0 keeps all.
|
|
265
|
+
function normalizeActivityItems(items, cap = LIVE_ACTIVITY_CAP) {
|
|
159
266
|
const normalized = Array.isArray(items)
|
|
160
267
|
? items.map((item) => normalizeRelayActivityEntry(item)).filter(Boolean)
|
|
161
268
|
: [];
|
|
162
|
-
return normalized
|
|
269
|
+
return capLiveActivityEntries(normalized, cap);
|
|
163
270
|
}
|
|
164
271
|
|
|
272
|
+
// A numbered row is the row with that number (the same tool line twice is two
|
|
273
|
+
// rows; a replayed event is one); an unnumbered one is known by its text.
|
|
165
274
|
function activityEntryKey(item) {
|
|
166
275
|
const entry = normalizeRelayActivityEntry(item);
|
|
167
276
|
if (!entry) return '';
|
|
277
|
+
if (entry.turnSeq !== undefined) return `#${entry.turnSeq}`;
|
|
168
278
|
return `${entry.subagentRunId || ''}::${entry.text}`;
|
|
169
279
|
}
|
|
170
280
|
|
|
281
|
+
const isNumbered = (item) => normalizeRelayActivityEntry(item)?.turnSeq !== undefined;
|
|
282
|
+
|
|
171
283
|
function isSubsequence(subset, sequence) {
|
|
172
284
|
if (!subset.length) return true;
|
|
173
285
|
let cursor = 0;
|
|
@@ -178,11 +290,17 @@ function isSubsequence(subset, sequence) {
|
|
|
178
290
|
return false;
|
|
179
291
|
}
|
|
180
292
|
|
|
181
|
-
export function mergeRelayActivityTexts(existingItems, incomingItems, limit =
|
|
293
|
+
export function mergeRelayActivityTexts(existingItems, incomingItems, limit = LIVE_ACTIVITY_CAP) {
|
|
182
294
|
const existing = normalizeActivityItems(existingItems, Number.POSITIVE_INFINITY);
|
|
183
295
|
const incoming = normalizeActivityItems(incomingItems, Number.POSITIVE_INFINITY);
|
|
184
296
|
if (!existing.length) return normalizeActivityItems(incoming, limit);
|
|
185
297
|
if (!incoming.length) return normalizeActivityItems(existing, limit);
|
|
298
|
+
// Both sides numbered (migration 0038): the union by number, in order.
|
|
299
|
+
if (existing.every(isNumbered) && incoming.every(isNumbered)) {
|
|
300
|
+
const byTurnSeq = new Map();
|
|
301
|
+
for (const item of [...existing, ...incoming]) byTurnSeq.set(item.turnSeq, item);
|
|
302
|
+
return normalizeActivityItems([...byTurnSeq.values()].sort((a, b) => a.turnSeq - b.turnSeq), limit);
|
|
303
|
+
}
|
|
186
304
|
if (isSubsequence(existing, incoming)) return normalizeActivityItems(incoming, limit);
|
|
187
305
|
if (isSubsequence(incoming, existing)) return normalizeActivityItems(existing, limit);
|
|
188
306
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BASE, TOKEN, CLIENT_ID, IS_SHARE_PREVIEW, SHARE_PREVIEW_CONVERSATION_ID, SHARED_CONVERSATION_TOKEN, authHeaders, updateWorkspaceRootHints, applyContextUsageBar, readContextUsageRatio, currentConvId, conversations, setCliOnline, setCloudflaredTunnelState, setActiveRuntimeSessionCount, setRuntimeSessionBindingCount, setContextIndicatorMode, setServerPlatform, setUpdateState } from './store.js';
|
|
2
|
+
import { liveActivityCap } from './live-activity-cap.mjs';
|
|
2
3
|
|
|
3
4
|
// fetch() has no default timeout. Without one, a request issued while the app is
|
|
4
5
|
// being backgrounded can hang indefinitely on a half-open mobile connection and
|
|
@@ -981,6 +982,12 @@ export async function loadConversation(id, options = {}) {
|
|
|
981
982
|
if (afterTimestamp) params.set('afterTimestamp', afterTimestamp);
|
|
982
983
|
const aroundMessageId = String(options.aroundMessageId || '').trim();
|
|
983
984
|
if (aroundMessageId) params.set('aroundMessageId', aroundMessageId);
|
|
985
|
+
// The running turn's tool lines per agent this page shows ("Tool lines
|
|
986
|
+
// shown"; 0 = no limit): the relay sends no more of them.
|
|
987
|
+
params.set('activityCap', String(liveActivityCap()));
|
|
988
|
+
// The live poll's finished replies it already holds (held-details.mjs).
|
|
989
|
+
const heldDetails = String(options.heldDetails || '');
|
|
990
|
+
if (heldDetails) params.set('heldDetails', heldDetails);
|
|
984
991
|
const query = params.toString();
|
|
985
992
|
// `onErrorResponse` tells a conversation the relay does not know (404) from a failed load.
|
|
986
993
|
const onErrorResponse = typeof options.onErrorResponse === 'function' ? options.onErrorResponse : undefined;
|
|
@@ -16,7 +16,7 @@ import { isChatInteractionHeld } from './selection-guard.mjs';
|
|
|
16
16
|
import { questionCardHeadLabel } from './conversation-provider-indicator.mjs';
|
|
17
17
|
import { questionAcceptsFreeText } from './question-answer-rule.mjs';
|
|
18
18
|
import { beginOwnSend, followsTranscriptEnd, refreshJumpButton, scrollAfterOwnSend } from './transcript-jump-bottom-view.mjs';
|
|
19
|
-
import {
|
|
19
|
+
import { renderQuestionStubs } from './questions-panel-ui.js';
|
|
20
20
|
import { isConversationAutoOpenHeld } from './session-history-ui.js';
|
|
21
21
|
|
|
22
22
|
let relayQuestionRenderHash = '';
|
|
@@ -230,8 +230,9 @@ function scrollQuestionCardIntoMessages(el, target) {
|
|
|
230
230
|
export function renderRelayQuestions() {
|
|
231
231
|
const el = document.getElementById('messages');
|
|
232
232
|
if (!el) return;
|
|
233
|
-
// The questions that don't block
|
|
234
|
-
|
|
233
|
+
// The questions that don't block: their ๐ stubs, in their turns' bubbles
|
|
234
|
+
// or standing alone before the cards a turn waits on.
|
|
235
|
+
renderQuestionStubs();
|
|
235
236
|
const previousScrollTop = el.scrollTop;
|
|
236
237
|
// A card is shown to a reader who is at the end. Anyone further up stays
|
|
237
238
|
// where they are; the jump-to-bottom button says that a question waits.
|
|
@@ -63,6 +63,9 @@ import {
|
|
|
63
63
|
} from './transcript-viewport-anchor.mjs';
|
|
64
64
|
import { initTranscriptJumpToBottom, onTranscriptEndLeft } from './transcript-jump-bottom-view.mjs';
|
|
65
65
|
import { syncScrollOnSendToggle, updateScrollOnSendSetting } from './transcript-jump-bottom.mjs';
|
|
66
|
+
import { isThoughtsExpanded, syncThoughtsExpandedToggle, updateThoughtsExpandedSetting } from './thoughts-preference.mjs';
|
|
67
|
+
import { previewLiveActivityCap, syncLiveActivityCapSlider, updateLiveActivityCap } from './live-activity-cap.mjs';
|
|
68
|
+
import { STREAM_VIEW_CHANGE_EVENT, isStreamViewEnabled, syncStreamViewToggle, updateStreamViewSetting } from './stream-view-preference.mjs';
|
|
66
69
|
import { previewWindowRetention, syncWindowRetentionSlider, updateWindowRetention } from './conversation-window-retention.mjs';
|
|
67
70
|
import { conversationPayloadCache, mergeReloadIntoWindow } from './conversation-payload-cache.mjs';
|
|
68
71
|
import {
|
|
@@ -93,7 +96,7 @@ import {
|
|
|
93
96
|
reportSharedViewerPresence,
|
|
94
97
|
setNetworkRequestsEnabled,
|
|
95
98
|
} from './api-client.js';
|
|
96
|
-
import { loadConversations, refreshConversations, openConversation, renderConvList, applyLoadedConversationState, initConversationListLazyLoading, initConversationFilter } from './journal-view.js';
|
|
99
|
+
import { loadConversations, refreshConversations, openConversation, renderConvList, applyLoadedConversationState, heldDetailsStore, initConversationListLazyLoading, initConversationFilter } from './journal-view.js';
|
|
97
100
|
import {
|
|
98
101
|
newConversation,
|
|
99
102
|
confirmNewConversationModel,
|
|
@@ -150,6 +153,7 @@ import {
|
|
|
150
153
|
getHeldBackLiveRowIds,
|
|
151
154
|
captureConversationReadingAnchor,
|
|
152
155
|
canMergeIntoTranscript,
|
|
156
|
+
refreshStreamViewLayout,
|
|
153
157
|
} from './conversation-view.js';
|
|
154
158
|
import { loadKeepingLiveRows } from './transcript-reload-freshness.mjs';
|
|
155
159
|
import { backgroundTasksSnapshotMark } from './background-tasks-view.mjs';
|
|
@@ -250,6 +254,8 @@ import { closeAnnotateEditor } from './annotate-editor.js';
|
|
|
250
254
|
import { closeEmojiPicker } from './emoji-view.js';
|
|
251
255
|
import { closeChangeCwdMenu } from './cwd-picker.js';
|
|
252
256
|
import { closeRelayWatchPanel } from './relay-event-subscriptions-ui.js';
|
|
257
|
+
import { closeQuestionsPanel, initQuestionsPanel, openQuestionsPanel, setQuestionsConversation } from './questions-panel-ui.js';
|
|
258
|
+
import { questionsTwinLabel } from './questions-view.mjs';
|
|
253
259
|
import { closeRelaySchedulePanel } from './relay-schedules-ui.js';
|
|
254
260
|
import { isInboxViewActive } from './inbox-ui.js';
|
|
255
261
|
import { initScrollbarHover } from './scrollbar-hover.mjs';
|
|
@@ -261,6 +267,8 @@ import {
|
|
|
261
267
|
renderAutoCompactNotesHtml,
|
|
262
268
|
renderContextUsageHtml,
|
|
263
269
|
renderThinkingControlHtml,
|
|
270
|
+
renderThoughtsPreferenceHtml,
|
|
271
|
+
renderStreamViewPreferenceHtml,
|
|
264
272
|
thinkingEnabledFromKey,
|
|
265
273
|
thinkingEnabledToKey,
|
|
266
274
|
} from './context-usage-view.mjs';
|
|
@@ -3702,6 +3710,18 @@ function initThinkingControl() {
|
|
|
3702
3710
|
if (!body || body.dataset.thinkingBound === '1') return;
|
|
3703
3711
|
body.dataset.thinkingBound = '1';
|
|
3704
3712
|
body.addEventListener('click', (event) => {
|
|
3713
|
+
// ๐งต Stream view and ๐ญ Thoughts: settings of this browser, nothing goes
|
|
3714
|
+
// to the relay. (The stream view's buttons share the thoughts' look.)
|
|
3715
|
+
const streamViewBtn = event.target?.closest?.('.ctx-stream-view-btn');
|
|
3716
|
+
if (streamViewBtn && body.contains(streamViewBtn)) {
|
|
3717
|
+
updateStreamViewSetting(streamViewBtn.dataset.streamView === 'on');
|
|
3718
|
+
return;
|
|
3719
|
+
}
|
|
3720
|
+
const thoughtsBtn = event.target?.closest?.('.ctx-thoughts-btn[data-thoughts-expanded]');
|
|
3721
|
+
if (thoughtsBtn && body.contains(thoughtsBtn)) {
|
|
3722
|
+
updateThoughtsExpandedSetting(thoughtsBtn.dataset.thoughtsExpanded !== 'off');
|
|
3723
|
+
return;
|
|
3724
|
+
}
|
|
3705
3725
|
const btn = event.target?.closest?.('.ctx-thinking-btn');
|
|
3706
3726
|
if (!btn || !body.contains(btn)) return;
|
|
3707
3727
|
void applyThinkingSelection(btn.dataset.thinkingAxis, btn.dataset.thinkingValue);
|
|
@@ -3861,6 +3881,10 @@ async function loadContextSummaryAndRender(convId) {
|
|
|
3861
3881
|
thinkingDisplay: payload.thinkingDisplay ?? null,
|
|
3862
3882
|
})
|
|
3863
3883
|
: '';
|
|
3884
|
+
// Every provider streams thoughts somewhere; the row mirrors the Settings
|
|
3885
|
+
// toggle and is this browser's own.
|
|
3886
|
+
const thoughtsHtml = renderThoughtsPreferenceHtml({ expanded: isThoughtsExpanded() })
|
|
3887
|
+
+ renderStreamViewPreferenceHtml({ on: isStreamViewEnabled() });
|
|
3864
3888
|
// What this folder's commits say, with the folder override: Claude (on
|
|
3865
3889
|
// either billing), and a Claude Cloud chat that was started from a folder
|
|
3866
3890
|
// (no path, no row).
|
|
@@ -3885,10 +3909,10 @@ async function loadContextSummaryAndRender(convId) {
|
|
|
3885
3909
|
// The structured breakdown is the answer; the runtime's own text dump stays
|
|
3886
3910
|
// available underneath for the details it carries that categories don't.
|
|
3887
3911
|
const bodyHtml = usageHtml
|
|
3888
|
-
? `${usageHtml}${autoCompactHtml}${thinkingHtml}${attributionHtml}${agentSessionsHtml}${detailText
|
|
3912
|
+
? `${usageHtml}${autoCompactHtml}${thinkingHtml}${thoughtsHtml}${attributionHtml}${agentSessionsHtml}${detailText
|
|
3889
3913
|
? `<details class="ctx-usage-raw"><summary>Raw details</summary><pre>${escHtml(detailText)}</pre></details>`
|
|
3890
3914
|
: ''}`
|
|
3891
|
-
: `${autoCompactHtml}${thinkingHtml}${attributionHtml}${agentSessionsHtml}<pre>${escHtml(detailText || 'No context data available.')}</pre>`;
|
|
3915
|
+
: `${autoCompactHtml}${thinkingHtml}${thoughtsHtml}${attributionHtml}${agentSessionsHtml}<pre>${escHtml(detailText || 'No context data available.')}</pre>`;
|
|
3892
3916
|
|
|
3893
3917
|
renderSummaryModalContent({
|
|
3894
3918
|
title: 'Context usage',
|
|
@@ -4130,12 +4154,17 @@ async function pollAuthenticatedCurrentConversationLive() {
|
|
|
4130
4154
|
const requestLimit = Math.max(20, getConversationLoadedMessageCount() || 0, savedLoadedCount || 0);
|
|
4131
4155
|
const tasksMark = backgroundTasksSnapshotMark(currentId);
|
|
4132
4156
|
const jumpEpoch = getTranscriptJumpEpoch();
|
|
4157
|
+
// The finished replies this page already holds go out without their
|
|
4158
|
+
// tool lines and thoughts (held-details.mjs); the answer gets them back
|
|
4159
|
+
// from the store, or is dropped if it names one the store lacks.
|
|
4160
|
+
const heldDetails = heldDetailsStore.param(currentId);
|
|
4133
4161
|
const { response, stale } = await loadKeepingLiveRows(
|
|
4134
|
-
() => loadConversation(currentId, { limit: requestLimit }),
|
|
4162
|
+
() => loadConversation(currentId, { limit: requestLimit, heldDetails }),
|
|
4135
4163
|
getTranscriptLiveRowEpoch,
|
|
4136
4164
|
);
|
|
4137
4165
|
if (!response) return;
|
|
4138
4166
|
if (String(currentConvId || '').trim() !== currentId) return;
|
|
4167
|
+
if (!heldDetailsStore.restore(currentId, response)) return;
|
|
4139
4168
|
// The user jumped into the history while this request was on its way: the
|
|
4140
4169
|
// answer is the end of the conversation as it was asked for before that,
|
|
4141
4170
|
// and would overwrite the window they jumped to. The next round asks again.
|
|
@@ -4155,6 +4184,7 @@ async function pollAuthenticatedCurrentConversationLive() {
|
|
|
4155
4184
|
followLiveUpdates: preserveBottom,
|
|
4156
4185
|
mergeFrom: merge?.mergeFrom || null,
|
|
4157
4186
|
backgroundTasksAsOf: tasksMark,
|
|
4187
|
+
fromPoll: true,
|
|
4158
4188
|
});
|
|
4159
4189
|
// A merge does not scroll as the full render does: a reader at the end
|
|
4160
4190
|
// is taken to the new end here.
|
|
@@ -4600,6 +4630,7 @@ function headerPriorityActionAvailability() {
|
|
|
4600
4630
|
const fullscreen = document.getElementById('fullscreen-btn');
|
|
4601
4631
|
return resolveHeaderActionAvailability({
|
|
4602
4632
|
sharedMode: isSharedReaderMode(),
|
|
4633
|
+
questionCount: Number(document.getElementById('questions-btn')?.dataset.count || 0),
|
|
4603
4634
|
pinCount: Number(pins?.dataset.count || 0),
|
|
4604
4635
|
hiddenCount: Number(hiddenBtn?.dataset.count || 0),
|
|
4605
4636
|
fullscreenAvailable: fullscreen?.dataset.headerAvailable === '1',
|
|
@@ -4676,6 +4707,8 @@ function headerFitSignature(header, availability) {
|
|
|
4676
4707
|
banner?.className, banner?.hidden, banner?.textContent,
|
|
4677
4708
|
document.getElementById('sidebar-toggle')?.hidden,
|
|
4678
4709
|
document.getElementById('chat-actions-menu-btn')?.hidden,
|
|
4710
|
+
document.getElementById('questions-btn')?.dataset.count,
|
|
4711
|
+
document.getElementById('questions-btn')?.dataset.urgent,
|
|
4679
4712
|
document.getElementById('pinned-messages-btn')?.dataset.count,
|
|
4680
4713
|
document.getElementById('hidden-messages-btn')?.dataset.count,
|
|
4681
4714
|
document.getElementById('fullscreen-btn')?.textContent,
|
|
@@ -4782,6 +4815,14 @@ function syncHeaderPriorityTwinLabels() {
|
|
|
4782
4815
|
const count = Number(document.getElementById(buttonId)?.dataset.count || 0);
|
|
4783
4816
|
twin.textContent = count ? `${icon} ${text} (${count})` : `${icon} ${text}`;
|
|
4784
4817
|
};
|
|
4818
|
+
const questions = document.getElementById('questions-btn');
|
|
4819
|
+
const questionsTwin = document.getElementById('chat-menu-questions');
|
|
4820
|
+
if (questionsTwin) {
|
|
4821
|
+
questionsTwin.textContent = questionsTwinLabel({
|
|
4822
|
+
pending: Number(questions?.dataset.count || 0),
|
|
4823
|
+
urgent: Number(questions?.dataset.urgent || 0),
|
|
4824
|
+
});
|
|
4825
|
+
}
|
|
4785
4826
|
label('pinned-messages-btn', 'chat-menu-pins', 'Pinned messages', '๐');
|
|
4786
4827
|
label('hidden-messages-btn', 'chat-menu-hidden', 'Hidden from shared viewers', '๐');
|
|
4787
4828
|
}
|
|
@@ -4812,6 +4853,11 @@ function scheduleHeaderPrioritySync({ force = false } = {}) {
|
|
|
4812
4853
|
// syncChatTitleControls.
|
|
4813
4854
|
function initHeaderPriorityActions({ sharedMode = false } = {}) {
|
|
4814
4855
|
if (!sharedMode) {
|
|
4856
|
+
// ๐'s own button toggles the list under it (questions-panel-ui.js); the
|
|
4857
|
+
// twin opens it under ๏ธ.
|
|
4858
|
+
bindHeaderPriorityAction('questions', () => {
|
|
4859
|
+
openQuestionsPanel({ anchor: document.getElementById('chat-actions-menu-btn') });
|
|
4860
|
+
}, { inline: false });
|
|
4815
4861
|
bindHeaderPriorityAction('pins', () => openPinnedMessagesModal(), { inline: false });
|
|
4816
4862
|
bindHeaderPriorityAction('hidden', () => openHiddenMessagesModal(), { inline: false });
|
|
4817
4863
|
bindHeaderPriorityAction('search', () => openMessageSearchModal(), { inline: false });
|
|
@@ -5052,6 +5098,7 @@ function syncChatTitleControls() {
|
|
|
5052
5098
|
if (!convId) {
|
|
5053
5099
|
setPinsConversation(null);
|
|
5054
5100
|
setHiddenMessagesConversation(null);
|
|
5101
|
+
setQuestionsConversation(null);
|
|
5055
5102
|
}
|
|
5056
5103
|
const conversation = convId ? (conversations[convId] || null) : null;
|
|
5057
5104
|
const sdkSessionId = String(conversation?.sdkSessionId || '').trim();
|
|
@@ -5333,6 +5380,9 @@ async function initApp() {
|
|
|
5333
5380
|
if (!sharedMode) {
|
|
5334
5381
|
syncSuspendHostVisibility();
|
|
5335
5382
|
syncScrollOnSendToggle();
|
|
5383
|
+
syncThoughtsExpandedToggle();
|
|
5384
|
+
syncLiveActivityCapSlider();
|
|
5385
|
+
syncStreamViewToggle();
|
|
5336
5386
|
syncWindowRetentionSlider();
|
|
5337
5387
|
initHostSuspendUi();
|
|
5338
5388
|
initUsageLimitUi();
|
|
@@ -5734,6 +5784,7 @@ async function initApp() {
|
|
|
5734
5784
|
void loadRelaySchedules();
|
|
5735
5785
|
// The Inbox's count, and the cards of questions that don't block.
|
|
5736
5786
|
startInbox();
|
|
5787
|
+
initQuestionsPanel();
|
|
5737
5788
|
if (pushConversationId) {
|
|
5738
5789
|
await openConversation(pushConversationId, { history: 'replace' }).catch(() => {});
|
|
5739
5790
|
}
|
|
@@ -5812,6 +5863,7 @@ async function bootstrap() {
|
|
|
5812
5863
|
closeTmuxInspector: closeTmuxInspectorView,
|
|
5813
5864
|
closeAnnotateEditor,
|
|
5814
5865
|
closeRelayWatchPanel,
|
|
5866
|
+
closeQuestionsPanel,
|
|
5815
5867
|
closeRelaySchedulePanel,
|
|
5816
5868
|
closeEmojiPicker,
|
|
5817
5869
|
});
|
|
@@ -5900,6 +5952,12 @@ window.resetGrokAllowanceAccounting = resetGrokAllowanceAccounting;
|
|
|
5900
5952
|
window.toggleCursorProvider = toggleCursorProvider;
|
|
5901
5953
|
window.updateShowSuspendHostSetting = updateShowSuspendHostSetting;
|
|
5902
5954
|
window.updateScrollOnSendSetting = (enabled) => updateScrollOnSendSetting(enabled);
|
|
5955
|
+
window.updateThoughtsExpandedSetting = (expanded) => updateThoughtsExpandedSetting(expanded);
|
|
5956
|
+
window.updateStreamViewSetting = (on) => updateStreamViewSetting(on);
|
|
5957
|
+
// The transcript and the live bubble take the other layout at once.
|
|
5958
|
+
window.addEventListener(STREAM_VIEW_CHANGE_EVENT, () => refreshStreamViewLayout());
|
|
5959
|
+
window.previewLiveActivityCap = (position) => previewLiveActivityCap(position);
|
|
5960
|
+
window.updateLiveActivityCap = (position) => updateLiveActivityCap(position);
|
|
5903
5961
|
window.previewConversationWindowRetention = (position) => previewWindowRetention(position);
|
|
5904
5962
|
// A lower cap cuts the kept windows down now; the open conversation's is cut
|
|
5905
5963
|
// when it is left.
|
|
@@ -47,6 +47,7 @@ export const CHAT_ACTIONS_MENU_ENTRY_IDS = Object.freeze([
|
|
|
47
47
|
'chat-menu-schedules',
|
|
48
48
|
'chat-menu-origin',
|
|
49
49
|
'chat-menu-edit-title',
|
|
50
|
+
'chat-menu-questions',
|
|
50
51
|
'chat-menu-pins',
|
|
51
52
|
'chat-menu-hidden',
|
|
52
53
|
'chat-menu-search',
|
|
@@ -82,6 +83,7 @@ const ALWAYS_HIDDEN_IDS = new Set(['chat-menu-compact', 'chat-menu-refresh-histo
|
|
|
82
83
|
* with HEADER_PRIORITY_MENU_IDS by the tests.
|
|
83
84
|
*/
|
|
84
85
|
export const CHAT_ACTIONS_MENU_TWIN_IDS = Object.freeze([
|
|
86
|
+
'chat-menu-questions',
|
|
85
87
|
'chat-menu-pins',
|
|
86
88
|
'chat-menu-hidden',
|
|
87
89
|
'chat-menu-search',
|
|
@@ -350,6 +350,74 @@ ${renderThinkingButtons('display', THINKING_DISPLAY_CHOICES, displayKey)}
|
|
|
350
350
|
`;
|
|
351
351
|
}
|
|
352
352
|
|
|
353
|
+
const THOUGHTS_EXPANDED_CHOICES = Object.freeze([
|
|
354
|
+
{ key: 'on', label: 'Expanded' },
|
|
355
|
+
{ key: 'off', label: 'Collapsed' },
|
|
356
|
+
]);
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* The mirror of Settings โบ General "Expand thoughts while an agent works", for
|
|
360
|
+
* every provider. A setting of this browser, so nothing goes to the relay:
|
|
361
|
+
* the buttons write thoughts-preference.mjs and both places follow it.
|
|
362
|
+
*
|
|
363
|
+
* @returns {string} HTML
|
|
364
|
+
*/
|
|
365
|
+
export function renderThoughtsPreferenceHtml({ expanded = true } = {}) {
|
|
366
|
+
const activeKey = expanded === false ? 'off' : 'on';
|
|
367
|
+
const buttons = THOUGHTS_EXPANDED_CHOICES.map(({ key, label }) => `
|
|
368
|
+
<button
|
|
369
|
+
type="button"
|
|
370
|
+
class="ctx-thoughts-btn${key === activeKey ? ' is-active' : ''}"
|
|
371
|
+
data-thoughts-expanded="${key}"
|
|
372
|
+
aria-pressed="${key === activeKey ? 'true' : 'false'}"
|
|
373
|
+
>${escapeHtml(label)}</button>`).join('');
|
|
374
|
+
return `
|
|
375
|
+
<div class="ctx-thinking ctx-thoughts">
|
|
376
|
+
<div class="ctx-thinking-row">
|
|
377
|
+
<span class="ctx-thinking-label">๐ญ Thoughts</span>
|
|
378
|
+
<div class="ctx-thinking-group" role="group" aria-label="Thoughts while an agent works">
|
|
379
|
+
${buttons}
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="ctx-thinking-note">Whether thoughts open while an agent works, in sub-agent bubbles and in the sub-agent viewer. A thought you fold or open by hand stays that way. Saved in this browser.</div>
|
|
383
|
+
</div>
|
|
384
|
+
`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const STREAM_VIEW_CHOICES = Object.freeze([
|
|
388
|
+
{ key: 'on', label: 'On' },
|
|
389
|
+
{ key: 'off', label: 'Off' },
|
|
390
|
+
]);
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* The mirror of Settings โบ General "Stream view", for every provider. A
|
|
394
|
+
* setting of this browser (stream-view-preference.mjs); nothing goes to the
|
|
395
|
+
* relay.
|
|
396
|
+
*
|
|
397
|
+
* @returns {string} HTML
|
|
398
|
+
*/
|
|
399
|
+
export function renderStreamViewPreferenceHtml({ on = false } = {}) {
|
|
400
|
+
const activeKey = on === true ? 'on' : 'off';
|
|
401
|
+
const buttons = STREAM_VIEW_CHOICES.map(({ key, label }) => `
|
|
402
|
+
<button
|
|
403
|
+
type="button"
|
|
404
|
+
class="ctx-thoughts-btn ctx-stream-view-btn${key === activeKey ? ' is-active' : ''}"
|
|
405
|
+
data-stream-view="${key}"
|
|
406
|
+
aria-pressed="${key === activeKey ? 'true' : 'false'}"
|
|
407
|
+
>${escapeHtml(label)}</button>`).join('');
|
|
408
|
+
return `
|
|
409
|
+
<div class="ctx-thinking ctx-stream-view">
|
|
410
|
+
<div class="ctx-thinking-row">
|
|
411
|
+
<span class="ctx-thinking-label">๐งต Stream view</span>
|
|
412
|
+
<div class="ctx-thinking-group" role="group" aria-label="Stream view">
|
|
413
|
+
${buttons}
|
|
414
|
+
</div>
|
|
415
|
+
</div>
|
|
416
|
+
<div class="ctx-thinking-note">Thoughts, tool calls and the reply of a turn as one list in the order they happened. Turns recorded before this version keep the grouped layout. Saved in this browser.</div>
|
|
417
|
+
</div>
|
|
418
|
+
`;
|
|
419
|
+
}
|
|
420
|
+
|
|
353
421
|
/**
|
|
354
422
|
* The owner's switch for a session an agent started: may its agents start
|
|
355
423
|
* sessions themselves (the relay_tool's one-level rule lifted for it)?
|