pi-studio 0.5.51 → 0.5.53
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 +27 -0
- package/README.md +4 -3
- package/client/studio-client.js +1117 -86
- package/client/studio.css +248 -9
- package/index.ts +354 -3
- package/package.json +1 -1
- package/shared/studio-markdown-latex-literals.js +203 -0
package/client/studio-client.js
CHANGED
|
@@ -61,6 +61,10 @@
|
|
|
61
61
|
const sourceBadgeEl = document.getElementById("sourceBadge");
|
|
62
62
|
const syncBadgeEl = document.getElementById("syncBadge");
|
|
63
63
|
let critiqueViewEl = document.getElementById("critiqueView");
|
|
64
|
+
const responseActionsEl = document.getElementById("responseActions");
|
|
65
|
+
const responseWrapEl = responseActionsEl && typeof responseActionsEl.closest === "function"
|
|
66
|
+
? responseActionsEl.closest(".response-wrap")
|
|
67
|
+
: null;
|
|
64
68
|
const referenceBadgeEl = document.getElementById("referenceBadge");
|
|
65
69
|
const editorViewSelect = document.getElementById("editorViewSelect");
|
|
66
70
|
const rightViewSelect = document.getElementById("rightViewSelect");
|
|
@@ -104,6 +108,7 @@
|
|
|
104
108
|
const leftFocusBtn = document.getElementById("leftFocusBtn");
|
|
105
109
|
const rightFocusBtn = document.getElementById("rightFocusBtn");
|
|
106
110
|
const reviewNotesBtn = document.getElementById("reviewNotesBtn");
|
|
111
|
+
const outlineBtn = document.getElementById("outlineBtn");
|
|
107
112
|
const scratchpadBtn = document.getElementById("scratchpadBtn");
|
|
108
113
|
const scratchpadOverlayEl = document.getElementById("scratchpadOverlay");
|
|
109
114
|
const scratchpadDialogEl = document.getElementById("scratchpadDialog");
|
|
@@ -114,6 +119,13 @@
|
|
|
114
119
|
const scratchpadClearBtn = document.getElementById("scratchpadClearBtn");
|
|
115
120
|
const scratchpadCloseBtn = document.getElementById("scratchpadCloseBtn");
|
|
116
121
|
const scratchpadDoneBtn = document.getElementById("scratchpadDoneBtn");
|
|
122
|
+
const outlineOverlayEl = document.getElementById("outlineOverlay");
|
|
123
|
+
const outlineDialogEl = document.getElementById("outlineDialog");
|
|
124
|
+
const outlineMetaEl = document.getElementById("outlineMeta");
|
|
125
|
+
const outlineListEl = document.getElementById("outlineList");
|
|
126
|
+
const outlineEmptyStateEl = document.getElementById("outlineEmptyState");
|
|
127
|
+
const outlineCloseBtn = document.getElementById("outlineCloseBtn");
|
|
128
|
+
const outlineDoneBtn = document.getElementById("outlineDoneBtn");
|
|
117
129
|
const reviewNotesOverlayEl = document.getElementById("reviewNotesOverlay");
|
|
118
130
|
const reviewNotesDialogEl = document.getElementById("reviewNotesDialog");
|
|
119
131
|
const reviewNotesMetaEl = document.getElementById("reviewNotesMeta");
|
|
@@ -171,6 +183,10 @@
|
|
|
171
183
|
let latestCritiqueNotesNormalized = "";
|
|
172
184
|
let responseHistory = [];
|
|
173
185
|
let responseHistoryIndex = -1;
|
|
186
|
+
let traceState = null;
|
|
187
|
+
let traceFilter = "all";
|
|
188
|
+
let traceAutoScroll = true;
|
|
189
|
+
let traceRenderRaf = null;
|
|
174
190
|
let studioRunChainActive = false;
|
|
175
191
|
let queuedSteeringCount = 0;
|
|
176
192
|
let agentBusyFromServer = false;
|
|
@@ -223,6 +239,262 @@
|
|
|
223
239
|
return changed;
|
|
224
240
|
}
|
|
225
241
|
|
|
242
|
+
function createEmptyTraceState() {
|
|
243
|
+
return {
|
|
244
|
+
runId: null,
|
|
245
|
+
requestId: null,
|
|
246
|
+
requestKind: null,
|
|
247
|
+
status: "idle",
|
|
248
|
+
startedAt: null,
|
|
249
|
+
updatedAt: null,
|
|
250
|
+
entries: [],
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function normalizeTraceStatus(status) {
|
|
255
|
+
return status === "running" || status === "complete" ? status : "idle";
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function normalizeTraceEntryStatus(status) {
|
|
259
|
+
return status === "streaming" || status === "pending" || status === "complete" || status === "error"
|
|
260
|
+
? status
|
|
261
|
+
: "pending";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function normalizeTraceEntry(entry, fallbackIndex) {
|
|
265
|
+
if (!entry || typeof entry !== "object") return null;
|
|
266
|
+
if (entry.type === "assistant") {
|
|
267
|
+
return {
|
|
268
|
+
id: typeof entry.id === "string" && entry.id.trim() ? entry.id.trim() : ("trace-assistant-" + fallbackIndex),
|
|
269
|
+
type: "assistant",
|
|
270
|
+
startedAt: parseFiniteNumber(entry.startedAt) || Date.now(),
|
|
271
|
+
updatedAt: parseFiniteNumber(entry.updatedAt) || Date.now(),
|
|
272
|
+
thinking: typeof entry.thinking === "string" ? entry.thinking : "",
|
|
273
|
+
text: typeof entry.text === "string" ? entry.text : "",
|
|
274
|
+
status: normalizeTraceEntryStatus(entry.status),
|
|
275
|
+
stopReason: typeof entry.stopReason === "string" && entry.stopReason.trim() ? entry.stopReason.trim() : null,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
if (entry.type === "tool") {
|
|
279
|
+
return {
|
|
280
|
+
id: typeof entry.id === "string" && entry.id.trim() ? entry.id.trim() : ("trace-tool-" + fallbackIndex),
|
|
281
|
+
type: "tool",
|
|
282
|
+
toolCallId: typeof entry.toolCallId === "string" ? entry.toolCallId : ("tool-" + fallbackIndex),
|
|
283
|
+
toolName: typeof entry.toolName === "string" ? entry.toolName : "tool",
|
|
284
|
+
label: parseNonEmptyString(entry.label),
|
|
285
|
+
argsSummary: parseNonEmptyString(entry.argsSummary),
|
|
286
|
+
output: typeof entry.output === "string" ? entry.output : "",
|
|
287
|
+
startedAt: parseFiniteNumber(entry.startedAt) || Date.now(),
|
|
288
|
+
updatedAt: parseFiniteNumber(entry.updatedAt) || Date.now(),
|
|
289
|
+
status: normalizeTraceEntryStatus(entry.status),
|
|
290
|
+
isError: Boolean(entry.isError),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function normalizeTraceState(raw) {
|
|
297
|
+
const fallback = createEmptyTraceState();
|
|
298
|
+
if (!raw || typeof raw !== "object") return fallback;
|
|
299
|
+
const entries = Array.isArray(raw.entries)
|
|
300
|
+
? raw.entries.map((entry, index) => normalizeTraceEntry(entry, index)).filter(Boolean)
|
|
301
|
+
: [];
|
|
302
|
+
return {
|
|
303
|
+
runId: parseNonEmptyString(raw.runId),
|
|
304
|
+
requestId: parseNonEmptyString(raw.requestId),
|
|
305
|
+
requestKind: parseNonEmptyString(raw.requestKind),
|
|
306
|
+
status: normalizeTraceStatus(raw.status),
|
|
307
|
+
startedAt: parseFiniteNumber(raw.startedAt),
|
|
308
|
+
updatedAt: parseFiniteNumber(raw.updatedAt),
|
|
309
|
+
entries,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function ensureTraceState() {
|
|
314
|
+
if (!traceState) traceState = createEmptyTraceState();
|
|
315
|
+
return traceState;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function replaceTraceState(nextState) {
|
|
319
|
+
traceState = normalizeTraceState(nextState);
|
|
320
|
+
renderTraceViewIfActive();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function upsertTraceEntry(entry) {
|
|
324
|
+
const normalized = normalizeTraceEntry(entry, ensureTraceState().entries.length);
|
|
325
|
+
if (!normalized) return;
|
|
326
|
+
const state = ensureTraceState();
|
|
327
|
+
const index = state.entries.findIndex((candidate) => candidate.id === normalized.id);
|
|
328
|
+
if (index >= 0) {
|
|
329
|
+
state.entries[index] = normalized;
|
|
330
|
+
} else {
|
|
331
|
+
state.entries.push(normalized);
|
|
332
|
+
}
|
|
333
|
+
state.updatedAt = normalized.updatedAt;
|
|
334
|
+
renderTraceViewIfActive();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function appendTraceAssistantDelta(entryId, deltaKind, delta, updatedAt) {
|
|
338
|
+
if (typeof delta !== "string" || !delta) return;
|
|
339
|
+
const state = ensureTraceState();
|
|
340
|
+
const targetId = typeof entryId === "string" && entryId.trim() ? entryId.trim() : null;
|
|
341
|
+
let entry = targetId ? state.entries.find((candidate) => candidate.id === targetId) : null;
|
|
342
|
+
if (!entry || entry.type !== "assistant") {
|
|
343
|
+
entry = normalizeTraceEntry({
|
|
344
|
+
id: targetId || ("trace-assistant-live-" + Date.now()),
|
|
345
|
+
type: "assistant",
|
|
346
|
+
startedAt: updatedAt,
|
|
347
|
+
updatedAt,
|
|
348
|
+
thinking: "",
|
|
349
|
+
text: "",
|
|
350
|
+
status: "streaming",
|
|
351
|
+
stopReason: null,
|
|
352
|
+
}, state.entries.length);
|
|
353
|
+
if (!entry) return;
|
|
354
|
+
state.entries.push(entry);
|
|
355
|
+
}
|
|
356
|
+
if (deltaKind === "thinking") {
|
|
357
|
+
entry.thinking += delta;
|
|
358
|
+
} else {
|
|
359
|
+
entry.text += delta;
|
|
360
|
+
}
|
|
361
|
+
entry.status = "streaming";
|
|
362
|
+
entry.updatedAt = parseFiniteNumber(updatedAt) || Date.now();
|
|
363
|
+
state.updatedAt = entry.updatedAt;
|
|
364
|
+
renderTraceViewIfActive();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function updateTraceStatusFromMessage(message) {
|
|
368
|
+
if (!message || typeof message !== "object") return;
|
|
369
|
+
const state = ensureTraceState();
|
|
370
|
+
state.runId = parseNonEmptyString(message.runId) || state.runId;
|
|
371
|
+
if (Object.prototype.hasOwnProperty.call(message, "requestId")) {
|
|
372
|
+
state.requestId = parseNonEmptyString(message.requestId);
|
|
373
|
+
}
|
|
374
|
+
if (Object.prototype.hasOwnProperty.call(message, "requestKind")) {
|
|
375
|
+
state.requestKind = parseNonEmptyString(message.requestKind);
|
|
376
|
+
}
|
|
377
|
+
if (Object.prototype.hasOwnProperty.call(message, "startedAt")) {
|
|
378
|
+
state.startedAt = parseFiniteNumber(message.startedAt);
|
|
379
|
+
}
|
|
380
|
+
if (Object.prototype.hasOwnProperty.call(message, "updatedAt")) {
|
|
381
|
+
state.updatedAt = parseFiniteNumber(message.updatedAt);
|
|
382
|
+
}
|
|
383
|
+
if (Object.prototype.hasOwnProperty.call(message, "status")) {
|
|
384
|
+
state.status = normalizeTraceStatus(message.status);
|
|
385
|
+
}
|
|
386
|
+
renderTraceViewIfActive();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function normalizeTraceFilter(filter) {
|
|
390
|
+
return filter === "thinking" || filter === "tools" ? filter : "all";
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function setTraceFilter(nextFilter) {
|
|
394
|
+
const normalized = normalizeTraceFilter(nextFilter);
|
|
395
|
+
if (traceFilter === normalized) return;
|
|
396
|
+
traceFilter = normalized;
|
|
397
|
+
traceAutoScroll = true;
|
|
398
|
+
renderTraceViewIfActive();
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function getTraceEntriesForFilter(filterOverride) {
|
|
402
|
+
const state = traceState || createEmptyTraceState();
|
|
403
|
+
const filter = normalizeTraceFilter(filterOverride || traceFilter);
|
|
404
|
+
const entries = Array.isArray(state.entries) ? state.entries : [];
|
|
405
|
+
if (filter === "tools") {
|
|
406
|
+
return entries.filter((entry) => entry.type === "tool");
|
|
407
|
+
}
|
|
408
|
+
if (filter === "thinking") {
|
|
409
|
+
return entries.filter((entry) => entry.type === "assistant" && String(entry.thinking || "").trim());
|
|
410
|
+
}
|
|
411
|
+
return entries.filter((entry) => {
|
|
412
|
+
if (entry.type === "assistant") {
|
|
413
|
+
return Boolean(String(entry.thinking || "").trim() || String(entry.text || "").trim());
|
|
414
|
+
}
|
|
415
|
+
return true;
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function buildVisibleWorkingText(filterOverride) {
|
|
420
|
+
const filter = normalizeTraceFilter(filterOverride || traceFilter);
|
|
421
|
+
const entries = getTraceEntriesForFilter(filter);
|
|
422
|
+
if (!entries.length) return "";
|
|
423
|
+
|
|
424
|
+
if (filter === "thinking") {
|
|
425
|
+
return entries
|
|
426
|
+
.map((entry) => entry && entry.type === "assistant" ? String(entry.thinking || "").trim() : "")
|
|
427
|
+
.filter(Boolean)
|
|
428
|
+
.join("\n\n");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return entries.map((entry) => {
|
|
432
|
+
if (entry.type === "assistant") {
|
|
433
|
+
const parts = [];
|
|
434
|
+
if (String(entry.thinking || "").trim()) {
|
|
435
|
+
parts.push("[Thinking]\n" + String(entry.thinking || "").trim());
|
|
436
|
+
}
|
|
437
|
+
if (filter === "all" && String(entry.text || "").trim()) {
|
|
438
|
+
parts.push("[Response]\n" + String(entry.text || "").trim());
|
|
439
|
+
}
|
|
440
|
+
return ["Assistant", ...parts].join("\n\n").trim();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const header = entry.label && entry.label !== entry.toolName
|
|
444
|
+
? ("Tool: " + String(entry.toolName || "tool") + " — " + entry.label)
|
|
445
|
+
: ("Tool: " + String(entry.toolName || "tool"));
|
|
446
|
+
const parts = [header];
|
|
447
|
+
if (String(entry.argsSummary || "").trim()) {
|
|
448
|
+
parts.push("Input:\n" + String(entry.argsSummary || "").trim());
|
|
449
|
+
}
|
|
450
|
+
if (String(entry.output || "").trim()) {
|
|
451
|
+
parts.push("Output:\n" + String(entry.output || "").trim());
|
|
452
|
+
}
|
|
453
|
+
return parts.join("\n\n").trim();
|
|
454
|
+
}).filter(Boolean).join("\n\n---\n\n");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function getWorkingDocumentLabel(filterOverride) {
|
|
458
|
+
const filter = normalizeTraceFilter(filterOverride || traceFilter);
|
|
459
|
+
if (filter === "thinking") return "working (thinking)";
|
|
460
|
+
if (filter === "tools") return "working (tools)";
|
|
461
|
+
return "working";
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
async function copyVisibleWorkingToClipboard() {
|
|
465
|
+
const content = buildVisibleWorkingText();
|
|
466
|
+
if (!content.trim()) {
|
|
467
|
+
setStatus("No visible working details to copy yet.", "warning");
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
try {
|
|
471
|
+
await navigator.clipboard.writeText(content);
|
|
472
|
+
setStatus("Copied visible working text.", "success");
|
|
473
|
+
} catch {
|
|
474
|
+
setStatus("Clipboard write failed.", "warning");
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function loadVisibleWorkingIntoEditor() {
|
|
479
|
+
const content = buildVisibleWorkingText();
|
|
480
|
+
if (!content.trim()) {
|
|
481
|
+
setStatus("No visible working details to load yet.", "warning");
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
setEditorText(content, { preserveScroll: false, preserveSelection: false });
|
|
485
|
+
setSourceState({ source: "blank", label: getWorkingDocumentLabel(), path: null });
|
|
486
|
+
setStatus("Loaded visible working into editor.", "success");
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function renderTraceViewIfActive() {
|
|
490
|
+
if (rightView !== "trace") return;
|
|
491
|
+
if (traceRenderRaf !== null) return;
|
|
492
|
+
traceRenderRaf = window.requestAnimationFrame(() => {
|
|
493
|
+
traceRenderRaf = null;
|
|
494
|
+
refreshResponseUi();
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
|
|
226
498
|
contextTokens = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextTokens : null);
|
|
227
499
|
contextWindow = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextWindow : null);
|
|
228
500
|
contextPercent = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextPercent : null);
|
|
@@ -305,6 +577,8 @@
|
|
|
305
577
|
let reviewNotesReturnFocusEl = null;
|
|
306
578
|
let reviewNotesPersistTimer = null;
|
|
307
579
|
let reviewNotesLoadNonce = 0;
|
|
580
|
+
let outlineEntries = [];
|
|
581
|
+
let outlineReturnFocusEl = null;
|
|
308
582
|
let pendingReviewNoteFocusId = null;
|
|
309
583
|
let pendingReviewNoteInlineFocusId = null;
|
|
310
584
|
let activePreviewCommentSelection = null;
|
|
@@ -402,6 +676,17 @@
|
|
|
402
676
|
if (typeof message.label === "string") summary.label = message.label;
|
|
403
677
|
if (Array.isArray(message.responseHistory)) summary.responseHistoryCount = message.responseHistory.length;
|
|
404
678
|
if (Array.isArray(message.items)) summary.itemsCount = message.items.length;
|
|
679
|
+
if (message.traceState && typeof message.traceState === "object" && Array.isArray(message.traceState.entries)) {
|
|
680
|
+
summary.traceEntries = message.traceState.entries.length;
|
|
681
|
+
summary.traceStatus = message.traceState.status;
|
|
682
|
+
}
|
|
683
|
+
if (message.trace && typeof message.trace === "object" && Array.isArray(message.trace.entries)) {
|
|
684
|
+
summary.traceEntries = message.trace.entries.length;
|
|
685
|
+
summary.traceStatus = message.trace.status;
|
|
686
|
+
}
|
|
687
|
+
if (typeof message.entryId === "string") summary.entryId = message.entryId;
|
|
688
|
+
if (typeof message.deltaKind === "string") summary.deltaKind = message.deltaKind;
|
|
689
|
+
if (typeof message.delta === "string") summary.deltaLength = message.delta.length;
|
|
405
690
|
if (typeof message.details === "object" && message.details !== null) summary.details = message.details;
|
|
406
691
|
return summary;
|
|
407
692
|
}
|
|
@@ -1130,6 +1415,12 @@
|
|
|
1130
1415
|
&& typeof reviewNotesDialogEl.contains === "function"
|
|
1131
1416
|
&& reviewNotesDialogEl.contains(event.target)
|
|
1132
1417
|
);
|
|
1418
|
+
const outlineOwnsEvent = Boolean(
|
|
1419
|
+
outlineDialogEl
|
|
1420
|
+
&& event.target
|
|
1421
|
+
&& typeof outlineDialogEl.contains === "function"
|
|
1422
|
+
&& outlineDialogEl.contains(event.target)
|
|
1423
|
+
);
|
|
1133
1424
|
|
|
1134
1425
|
if (isScratchpadOpen() && plainEscape) {
|
|
1135
1426
|
event.preventDefault();
|
|
@@ -1143,7 +1434,13 @@
|
|
|
1143
1434
|
return;
|
|
1144
1435
|
}
|
|
1145
1436
|
|
|
1146
|
-
if (
|
|
1437
|
+
if (isOutlineOpen() && plainEscape) {
|
|
1438
|
+
event.preventDefault();
|
|
1439
|
+
closeOutline();
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
if (scratchpadOwnsEvent || reviewNotesOwnsEvent || outlineOwnsEvent) {
|
|
1147
1444
|
return;
|
|
1148
1445
|
}
|
|
1149
1446
|
|
|
@@ -1397,6 +1694,21 @@
|
|
|
1397
1694
|
function updateReferenceBadge() {
|
|
1398
1695
|
if (!referenceBadgeEl) return;
|
|
1399
1696
|
|
|
1697
|
+
if (rightView === "trace") {
|
|
1698
|
+
const state = traceState || createEmptyTraceState();
|
|
1699
|
+
const entryCount = getTraceEntriesForFilter(traceFilter).length;
|
|
1700
|
+
const time = formatReferenceTime(state.startedAt || state.updatedAt);
|
|
1701
|
+
if (state.status === "idle") {
|
|
1702
|
+
referenceBadgeEl.textContent = "Working: no active run yet";
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
const statusLabel = state.status === "running" ? "live" : "complete";
|
|
1706
|
+
referenceBadgeEl.textContent = "Working: " + statusLabel
|
|
1707
|
+
+ (entryCount ? (" · " + entryCount + " entr" + (entryCount === 1 ? "y" : "ies")) : "")
|
|
1708
|
+
+ (time ? (" · " + time) : "");
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1400
1712
|
if (rightView === "editor-preview") {
|
|
1401
1713
|
const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
|
|
1402
1714
|
if (hasResponse) {
|
|
@@ -1410,26 +1722,6 @@
|
|
|
1410
1722
|
}
|
|
1411
1723
|
|
|
1412
1724
|
const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
|
|
1413
|
-
const hasThinking = Boolean(latestResponseThinking && latestResponseThinking.trim());
|
|
1414
|
-
if (rightView === "thinking") {
|
|
1415
|
-
if (!hasResponse && !hasThinking) {
|
|
1416
|
-
referenceBadgeEl.textContent = "Thinking: none";
|
|
1417
|
-
return;
|
|
1418
|
-
}
|
|
1419
|
-
|
|
1420
|
-
const time = formatReferenceTime(latestResponseTimestamp);
|
|
1421
|
-
const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
|
|
1422
|
-
const selected = total > 0 && responseHistoryIndex >= 0 && responseHistoryIndex < total
|
|
1423
|
-
? responseHistoryIndex + 1
|
|
1424
|
-
: 0;
|
|
1425
|
-
const historyPrefix = total > 0 ? "Response history " + selected + "/" + total + " · " : "";
|
|
1426
|
-
const thinkingLabel = hasThinking ? "assistant thinking" : "assistant thinking unavailable";
|
|
1427
|
-
referenceBadgeEl.textContent = time
|
|
1428
|
-
? historyPrefix + thinkingLabel + " · " + time
|
|
1429
|
-
: historyPrefix + thinkingLabel;
|
|
1430
|
-
return;
|
|
1431
|
-
}
|
|
1432
|
-
|
|
1433
1725
|
if (!hasResponse) {
|
|
1434
1726
|
referenceBadgeEl.textContent = "Latest response: none";
|
|
1435
1727
|
return;
|
|
@@ -1505,14 +1797,15 @@
|
|
|
1505
1797
|
function updateSyncBadge(normalizedEditorText) {
|
|
1506
1798
|
if (!syncBadgeEl) return;
|
|
1507
1799
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1800
|
+
if (rightView === "trace") {
|
|
1801
|
+
syncBadgeEl.hidden = true;
|
|
1802
|
+
syncBadgeEl.classList.remove("sync");
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1512
1805
|
|
|
1513
|
-
if (!
|
|
1806
|
+
if (!latestResponseHasContent) {
|
|
1514
1807
|
syncBadgeEl.hidden = true;
|
|
1515
|
-
syncBadgeEl.textContent =
|
|
1808
|
+
syncBadgeEl.textContent = "In sync with response";
|
|
1516
1809
|
syncBadgeEl.classList.remove("sync");
|
|
1517
1810
|
return;
|
|
1518
1811
|
}
|
|
@@ -1520,10 +1813,9 @@
|
|
|
1520
1813
|
const normalizedEditor = typeof normalizedEditorText === "string"
|
|
1521
1814
|
? normalizedEditorText
|
|
1522
1815
|
: normalizeForCompare(sourceTextEl.value);
|
|
1523
|
-
const
|
|
1524
|
-
const inSync = normalizedEditor === targetNormalized;
|
|
1816
|
+
const inSync = normalizedEditor === latestResponseNormalized;
|
|
1525
1817
|
syncBadgeEl.hidden = !inSync;
|
|
1526
|
-
syncBadgeEl.textContent =
|
|
1818
|
+
syncBadgeEl.textContent = "In sync with response";
|
|
1527
1819
|
|
|
1528
1820
|
if (inSync) {
|
|
1529
1821
|
syncBadgeEl.classList.add("sync");
|
|
@@ -2172,6 +2464,40 @@
|
|
|
2172
2464
|
});
|
|
2173
2465
|
}
|
|
2174
2466
|
|
|
2467
|
+
function handleTracePaneScroll() {
|
|
2468
|
+
if (rightView !== "trace") return;
|
|
2469
|
+
traceAutoScroll = shouldStickTraceToBottom();
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
async function handleTracePaneClick(event) {
|
|
2473
|
+
if (rightView !== "trace") return;
|
|
2474
|
+
const target = event.target;
|
|
2475
|
+
const filterBtn = target instanceof Element ? target.closest("[data-trace-filter]") : null;
|
|
2476
|
+
if (filterBtn) {
|
|
2477
|
+
event.preventDefault();
|
|
2478
|
+
const nextFilter = filterBtn.getAttribute("data-trace-filter") || "all";
|
|
2479
|
+
setTraceFilter(nextFilter);
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
const actionBtn = target instanceof Element ? target.closest("[data-trace-action]") : null;
|
|
2483
|
+
if (!actionBtn) return;
|
|
2484
|
+
event.preventDefault();
|
|
2485
|
+
const action = actionBtn.getAttribute("data-trace-action") || "";
|
|
2486
|
+
if (action === "copy") {
|
|
2487
|
+
await copyVisibleWorkingToClipboard();
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
if (action === "load") {
|
|
2491
|
+
loadVisibleWorkingIntoEditor();
|
|
2492
|
+
}
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
function attachResponsePaneInteractionHandlers() {
|
|
2496
|
+
if (!critiqueViewEl) return;
|
|
2497
|
+
critiqueViewEl.addEventListener("scroll", handleTracePaneScroll);
|
|
2498
|
+
critiqueViewEl.addEventListener("click", handleTracePaneClick);
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2175
2501
|
function replaceResponsePaneWithClone() {
|
|
2176
2502
|
const currentEl = critiqueViewEl;
|
|
2177
2503
|
if (!currentEl || !currentEl.parentNode || typeof currentEl.cloneNode !== "function") {
|
|
@@ -2185,6 +2511,7 @@
|
|
|
2185
2511
|
|
|
2186
2512
|
currentEl.parentNode.replaceChild(replacement, currentEl);
|
|
2187
2513
|
critiqueViewEl = replacement;
|
|
2514
|
+
attachResponsePaneInteractionHandlers();
|
|
2188
2515
|
return critiqueViewEl;
|
|
2189
2516
|
}
|
|
2190
2517
|
|
|
@@ -2680,7 +3007,134 @@
|
|
|
2680
3007
|
}, delay);
|
|
2681
3008
|
}
|
|
2682
3009
|
|
|
3010
|
+
function shouldStickTraceToBottom() {
|
|
3011
|
+
if (!critiqueViewEl) return true;
|
|
3012
|
+
const remaining = critiqueViewEl.scrollHeight - critiqueViewEl.scrollTop - critiqueViewEl.clientHeight;
|
|
3013
|
+
return remaining < 56;
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
function buildTracePanelHtml() {
|
|
3017
|
+
const state = traceState || createEmptyTraceState();
|
|
3018
|
+
const filter = normalizeTraceFilter(traceFilter);
|
|
3019
|
+
const entries = getTraceEntriesForFilter(filter);
|
|
3020
|
+
const visibleWorking = buildVisibleWorkingText(filter);
|
|
3021
|
+
const hasVisibleContent = Boolean(visibleWorking.trim());
|
|
3022
|
+
const started = formatReferenceTime(state.startedAt || state.updatedAt);
|
|
3023
|
+
const statusLabel = state.status === "running"
|
|
3024
|
+
? "Live"
|
|
3025
|
+
: (state.status === "complete" ? "Complete" : "Idle");
|
|
3026
|
+
const filterMeta = filter === "thinking"
|
|
3027
|
+
? "Thinking only"
|
|
3028
|
+
: (filter === "tools" ? "Tools only" : null);
|
|
3029
|
+
const toolbar = "<div class='trace-toolbar'>"
|
|
3030
|
+
+ "<div class='trace-summary'>"
|
|
3031
|
+
+ "<span class='trace-summary-badge'>Working</span>"
|
|
3032
|
+
+ "<span class='trace-summary-status trace-status-" + escapeHtml(String(state.status || "idle")) + "'>" + escapeHtml(statusLabel) + "</span>"
|
|
3033
|
+
+ (started ? ("<span class='trace-summary-meta'>Started " + escapeHtml(started) + "</span>") : "")
|
|
3034
|
+
+ (filterMeta ? ("<span class='trace-summary-meta'>" + escapeHtml(filterMeta) + "</span>") : "")
|
|
3035
|
+
+ "</div>"
|
|
3036
|
+
+ "<div class='trace-controls'>"
|
|
3037
|
+
+ "<div class='trace-filter-group' role='tablist' aria-label='Working components'>"
|
|
3038
|
+
+ "<button type='button' class='trace-filter-btn" + (filter === "all" ? " is-active" : "") + "' data-trace-filter='all' aria-pressed='" + (filter === "all" ? "true" : "false") + "'>All</button>"
|
|
3039
|
+
+ "<button type='button' class='trace-filter-btn" + (filter === "thinking" ? " is-active" : "") + "' data-trace-filter='thinking' aria-pressed='" + (filter === "thinking" ? "true" : "false") + "'>Thinking</button>"
|
|
3040
|
+
+ "<button type='button' class='trace-filter-btn" + (filter === "tools" ? " is-active" : "") + "' data-trace-filter='tools' aria-pressed='" + (filter === "tools" ? "true" : "false") + "'>Tools</button>"
|
|
3041
|
+
+ "</div>"
|
|
3042
|
+
+ "<button type='button' class='trace-action-btn' data-trace-action='load'" + (hasVisibleContent ? "" : " disabled") + ">Load visible into editor</button>"
|
|
3043
|
+
+ "<button type='button' class='trace-action-btn' data-trace-action='copy'" + (hasVisibleContent ? "" : " disabled") + ">Copy visible</button>"
|
|
3044
|
+
+ "</div>"
|
|
3045
|
+
+ "</div>";
|
|
3046
|
+
|
|
3047
|
+
if (!entries.length) {
|
|
3048
|
+
const emptyMessage = filter === "thinking"
|
|
3049
|
+
? "No thinking steps in this working view yet."
|
|
3050
|
+
: (filter === "tools"
|
|
3051
|
+
? "No tool steps in this working view yet."
|
|
3052
|
+
: (state.status === "running"
|
|
3053
|
+
? "Waiting for the first model or tool update…"
|
|
3054
|
+
: "No live working view yet. Start a run or critique to watch working details here."));
|
|
3055
|
+
return "<div class='trace-panel'>" + toolbar + "<div class='trace-empty'>" + escapeHtml(emptyMessage) + "</div></div>";
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
const cards = entries.map((entry) => {
|
|
3059
|
+
if (entry.type === "assistant") {
|
|
3060
|
+
const sections = [];
|
|
3061
|
+
if (String(entry.thinking || "").trim()) {
|
|
3062
|
+
sections.push(
|
|
3063
|
+
"<div class='trace-section'>"
|
|
3064
|
+
+ "<div class='trace-section-label'>Thinking</div>"
|
|
3065
|
+
+ "<pre class='plain-markdown trace-output'>" + escapeHtml(entry.thinking) + "</pre>"
|
|
3066
|
+
+ "</div>"
|
|
3067
|
+
);
|
|
3068
|
+
}
|
|
3069
|
+
if (filter === "all" && String(entry.text || "").trim()) {
|
|
3070
|
+
sections.push(
|
|
3071
|
+
"<div class='trace-section'>"
|
|
3072
|
+
+ "<div class='trace-section-label'>Response</div>"
|
|
3073
|
+
+ "<pre class='plain-markdown trace-output'>" + escapeHtml(entry.text) + "</pre>"
|
|
3074
|
+
+ "</div>"
|
|
3075
|
+
);
|
|
3076
|
+
}
|
|
3077
|
+
if (!sections.length) {
|
|
3078
|
+
sections.push("<div class='trace-empty-inline'>Waiting for streamed content…</div>");
|
|
3079
|
+
}
|
|
3080
|
+
return "<article class='trace-card trace-card-assistant'>"
|
|
3081
|
+
+ "<div class='trace-card-header'>"
|
|
3082
|
+
+ "<span class='trace-kind-badge'>" + escapeHtml(filter === "thinking" ? "Thinking" : "Assistant") + "</span>"
|
|
3083
|
+
+ "<span class='trace-card-meta'>" + escapeHtml(formatReferenceTime(entry.updatedAt) || "live") + "</span>"
|
|
3084
|
+
+ "<span class='trace-entry-status trace-entry-status-" + escapeHtml(entry.status) + "'>" + escapeHtml(entry.status === "streaming" ? "Live" : "Complete") + "</span>"
|
|
3085
|
+
+ (entry.stopReason ? ("<span class='trace-card-meta'>stop: " + escapeHtml(entry.stopReason) + "</span>") : "")
|
|
3086
|
+
+ "</div>"
|
|
3087
|
+
+ sections.join("")
|
|
3088
|
+
+ "</article>";
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
const title = entry.label || entry.toolName || "tool";
|
|
3092
|
+
const argsSummary = entry.argsSummary
|
|
3093
|
+
? "<div class='trace-section'><div class='trace-section-label'>Input</div><pre class='plain-markdown trace-output'>" + escapeHtml(entry.argsSummary) + "</pre></div>"
|
|
3094
|
+
: "";
|
|
3095
|
+
const output = entry.output
|
|
3096
|
+
? "<div class='trace-section'><div class='trace-section-label'>Output</div><pre class='plain-markdown trace-output'>" + escapeHtml(entry.output) + "</pre></div>"
|
|
3097
|
+
: "<div class='trace-empty-inline'>No output yet.</div>";
|
|
3098
|
+
const toolStatusLabel = entry.isError
|
|
3099
|
+
? "Error"
|
|
3100
|
+
: (entry.status === "streaming" || entry.status === "pending" ? "Live" : "Complete");
|
|
3101
|
+
return "<article class='trace-card trace-card-tool'>"
|
|
3102
|
+
+ "<div class='trace-card-header'>"
|
|
3103
|
+
+ "<span class='trace-kind-badge'>" + escapeHtml(entry.toolName || "tool") + "</span>"
|
|
3104
|
+
+ "<span class='trace-card-title'>" + escapeHtml(title) + "</span>"
|
|
3105
|
+
+ "<span class='trace-card-meta'>" + escapeHtml(formatReferenceTime(entry.updatedAt) || "live") + "</span>"
|
|
3106
|
+
+ "<span class='trace-entry-status trace-entry-status-" + escapeHtml(entry.status) + "'>" + escapeHtml(toolStatusLabel) + "</span>"
|
|
3107
|
+
+ "</div>"
|
|
3108
|
+
+ argsSummary
|
|
3109
|
+
+ output
|
|
3110
|
+
+ "</article>";
|
|
3111
|
+
}).join("");
|
|
3112
|
+
|
|
3113
|
+
return "<div class='trace-panel'>" + toolbar + "<div class='trace-list'>" + cards + "</div></div>";
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
function renderTraceView() {
|
|
3117
|
+
if (!critiqueViewEl) return;
|
|
3118
|
+
const shouldStick = traceAutoScroll || shouldStickTraceToBottom();
|
|
3119
|
+
const previousScrollTop = critiqueViewEl.scrollTop;
|
|
3120
|
+
finishPreviewRender(critiqueViewEl);
|
|
3121
|
+
critiqueViewEl.innerHTML = buildTracePanelHtml();
|
|
3122
|
+
critiqueViewEl.classList.remove("response-scroll-resetting");
|
|
3123
|
+
if (shouldStick) {
|
|
3124
|
+
critiqueViewEl.scrollTop = critiqueViewEl.scrollHeight;
|
|
3125
|
+
traceAutoScroll = true;
|
|
3126
|
+
} else {
|
|
3127
|
+
critiqueViewEl.scrollTop = previousScrollTop;
|
|
3128
|
+
}
|
|
3129
|
+
scheduleResponsePaneRepaintNudge();
|
|
3130
|
+
}
|
|
3131
|
+
|
|
2683
3132
|
function renderActiveResult() {
|
|
3133
|
+
if (rightView === "trace") {
|
|
3134
|
+
renderTraceView();
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
|
|
2684
3138
|
if (rightView === "editor-preview") {
|
|
2685
3139
|
const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
|
|
2686
3140
|
if (!editorText.trim()) {
|
|
@@ -2699,17 +3153,6 @@
|
|
|
2699
3153
|
return;
|
|
2700
3154
|
}
|
|
2701
3155
|
|
|
2702
|
-
if (rightView === "thinking") {
|
|
2703
|
-
const thinking = latestResponseThinking;
|
|
2704
|
-
finishPreviewRender(critiqueViewEl);
|
|
2705
|
-
critiqueViewEl.innerHTML = thinking && thinking.trim()
|
|
2706
|
-
? buildPlainMarkdownHtml(thinking)
|
|
2707
|
-
: "<pre class='plain-markdown'>No thinking available for this response.</pre>";
|
|
2708
|
-
applyPendingResponseScrollReset();
|
|
2709
|
-
scheduleResponsePaneRepaintNudge();
|
|
2710
|
-
return;
|
|
2711
|
-
}
|
|
2712
|
-
|
|
2713
3156
|
const markdown = latestResponseMarkdown;
|
|
2714
3157
|
if (!markdown || !markdown.trim()) {
|
|
2715
3158
|
finishPreviewRender(critiqueViewEl);
|
|
@@ -2753,55 +3196,43 @@
|
|
|
2753
3196
|
|
|
2754
3197
|
function updateResultActionButtons(normalizedEditorText) {
|
|
2755
3198
|
const hasResponse = latestResponseHasContent;
|
|
2756
|
-
const hasThinking = Boolean(latestResponseThinking && latestResponseThinking.trim());
|
|
2757
3199
|
const normalizedEditor = typeof normalizedEditorText === "string"
|
|
2758
3200
|
? normalizedEditorText
|
|
2759
3201
|
: normalizeForCompare(sourceTextEl.value);
|
|
2760
3202
|
const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
|
|
2761
|
-
const thinkingLoaded = hasThinking && normalizedEditor === latestResponseThinkingNormalized;
|
|
2762
3203
|
const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
|
|
2763
|
-
const
|
|
3204
|
+
const showingTrace = rightView === "trace";
|
|
3205
|
+
|
|
3206
|
+
if (responseWrapEl) {
|
|
3207
|
+
responseWrapEl.hidden = showingTrace;
|
|
3208
|
+
}
|
|
2764
3209
|
|
|
2765
3210
|
const critiqueNotes = isCritiqueResponse ? latestCritiqueNotes : "";
|
|
2766
3211
|
const critiqueNotesLoaded = Boolean(critiqueNotes) && normalizedEditor === latestCritiqueNotesNormalized;
|
|
2767
3212
|
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
loadCritiqueFullBtn.hidden = true;
|
|
2772
|
-
|
|
2773
|
-
loadResponseBtn.disabled = uiBusy || !hasThinking || thinkingLoaded;
|
|
2774
|
-
loadResponseBtn.textContent = !hasThinking
|
|
2775
|
-
? "Thinking unavailable"
|
|
2776
|
-
: (thinkingLoaded ? "Thinking already in editor" : "Load thinking into editor");
|
|
3213
|
+
loadResponseBtn.hidden = isCritiqueResponse;
|
|
3214
|
+
loadCritiqueNotesBtn.hidden = !isCritiqueResponse;
|
|
3215
|
+
loadCritiqueFullBtn.hidden = !isCritiqueResponse;
|
|
2777
3216
|
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
} else {
|
|
2781
|
-
loadResponseBtn.hidden = isCritiqueResponse;
|
|
2782
|
-
loadCritiqueNotesBtn.hidden = !isCritiqueResponse;
|
|
2783
|
-
loadCritiqueFullBtn.hidden = !isCritiqueResponse;
|
|
3217
|
+
loadResponseBtn.disabled = uiBusy || !hasResponse || responseLoaded || isCritiqueResponse;
|
|
3218
|
+
loadResponseBtn.textContent = responseLoaded ? "Response already in editor" : "Load response into editor";
|
|
2784
3219
|
|
|
2785
|
-
|
|
2786
|
-
|
|
3220
|
+
loadCritiqueNotesBtn.disabled = uiBusy || !isCritiqueResponse || !critiqueNotes || critiqueNotesLoaded;
|
|
3221
|
+
loadCritiqueNotesBtn.textContent = critiqueNotesLoaded ? "Critique notes already in editor" : "Load critique notes into editor";
|
|
2787
3222
|
|
|
2788
|
-
|
|
2789
|
-
|
|
3223
|
+
loadCritiqueFullBtn.disabled = uiBusy || !isCritiqueResponse || responseLoaded;
|
|
3224
|
+
loadCritiqueFullBtn.textContent = responseLoaded ? "Full critique already in editor" : "Load full critique into editor";
|
|
2790
3225
|
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
copyResponseBtn.disabled = uiBusy || !hasResponse;
|
|
2795
|
-
copyResponseBtn.textContent = "Copy response text";
|
|
2796
|
-
}
|
|
3226
|
+
copyResponseBtn.disabled = uiBusy || !hasResponse;
|
|
3227
|
+
copyResponseBtn.textContent = "Copy response text";
|
|
2797
3228
|
|
|
2798
3229
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
2799
3230
|
const exportText = rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown;
|
|
2800
3231
|
const canExportPdf = rightPaneShowsPreview && Boolean(String(exportText || "").trim());
|
|
2801
3232
|
if (exportPdfBtn) {
|
|
2802
3233
|
exportPdfBtn.disabled = uiBusy || pdfExportInProgress || !canExportPdf;
|
|
2803
|
-
if (rightView === "
|
|
2804
|
-
exportPdfBtn.title = "
|
|
3234
|
+
if (rightView === "trace") {
|
|
3235
|
+
exportPdfBtn.title = "Working view does not support PDF export.";
|
|
2805
3236
|
} else if (rightView === "markdown") {
|
|
2806
3237
|
exportPdfBtn.title = "Switch right pane to Response (Preview) or Editor (Preview) to export PDF.";
|
|
2807
3238
|
} else if (!canExportPdf) {
|
|
@@ -2991,6 +3422,7 @@
|
|
|
2991
3422
|
scheduleEditorMetaUpdate();
|
|
2992
3423
|
}
|
|
2993
3424
|
updateEditorSelectionCommentUi();
|
|
3425
|
+
updateOutlineUi();
|
|
2994
3426
|
}
|
|
2995
3427
|
|
|
2996
3428
|
function setEditorView(nextView) {
|
|
@@ -3025,15 +3457,20 @@
|
|
|
3025
3457
|
}
|
|
3026
3458
|
updateReviewNotesUi();
|
|
3027
3459
|
updateEditorSelectionCommentUi();
|
|
3460
|
+
updateOutlineUi();
|
|
3028
3461
|
}
|
|
3029
3462
|
|
|
3030
3463
|
function setRightView(nextView) {
|
|
3464
|
+
const previousView = rightView;
|
|
3031
3465
|
rightView = nextView === "preview"
|
|
3032
3466
|
? "preview"
|
|
3033
3467
|
: (nextView === "editor-preview"
|
|
3034
3468
|
? "editor-preview"
|
|
3035
|
-
: (nextView === "thinking" ? "
|
|
3469
|
+
: ((nextView === "trace" || nextView === "thinking") ? "trace" : "markdown"));
|
|
3036
3470
|
rightViewSelect.value = rightView;
|
|
3471
|
+
if (rightView === "trace" && previousView !== "trace") {
|
|
3472
|
+
traceAutoScroll = true;
|
|
3473
|
+
}
|
|
3037
3474
|
|
|
3038
3475
|
if (rightView !== "editor-preview" && responseEditorPreviewTimer) {
|
|
3039
3476
|
window.clearTimeout(responseEditorPreviewTimer);
|
|
@@ -4025,6 +4462,10 @@
|
|
|
4025
4462
|
return Boolean(scratchpadOverlayEl && !scratchpadOverlayEl.hidden);
|
|
4026
4463
|
}
|
|
4027
4464
|
|
|
4465
|
+
function isOutlineOpen() {
|
|
4466
|
+
return Boolean(outlineOverlayEl && !outlineOverlayEl.hidden);
|
|
4467
|
+
}
|
|
4468
|
+
|
|
4028
4469
|
function isReviewNotesOpen() {
|
|
4029
4470
|
return Boolean(reviewNotesOverlayEl && !reviewNotesOverlayEl.hidden);
|
|
4030
4471
|
}
|
|
@@ -4198,6 +4639,400 @@
|
|
|
4198
4639
|
};
|
|
4199
4640
|
}
|
|
4200
4641
|
|
|
4642
|
+
function buildOutlineLineIndex(text) {
|
|
4643
|
+
const source = String(text || "").replace(/\r\n/g, "\n");
|
|
4644
|
+
const lines = source.split("\n");
|
|
4645
|
+
const lineOffsets = [];
|
|
4646
|
+
let runningOffset = 0;
|
|
4647
|
+
for (const line of lines) {
|
|
4648
|
+
lineOffsets.push(runningOffset);
|
|
4649
|
+
runningOffset += line.length + 1;
|
|
4650
|
+
}
|
|
4651
|
+
return { source, lines, lineOffsets };
|
|
4652
|
+
}
|
|
4653
|
+
|
|
4654
|
+
function makeOutlineEntry(options) {
|
|
4655
|
+
const entry = options && typeof options === "object" ? options : {};
|
|
4656
|
+
const label = typeof entry.label === "string" ? entry.label.trim() : "";
|
|
4657
|
+
if (!label) return null;
|
|
4658
|
+
const selectionStart = Math.max(0, Math.floor(Number(entry.selectionStart) || 0));
|
|
4659
|
+
const selectionEnd = Math.max(selectionStart, Math.floor(Number(entry.selectionEnd) || selectionStart));
|
|
4660
|
+
return {
|
|
4661
|
+
id: typeof entry.id === "string" && entry.id ? entry.id : makeRequestId(),
|
|
4662
|
+
kind: typeof entry.kind === "string" && entry.kind ? entry.kind : "section",
|
|
4663
|
+
depth: Math.max(1, Math.floor(Number(entry.depth) || 1)),
|
|
4664
|
+
label,
|
|
4665
|
+
lineStart: Math.max(1, Math.floor(Number(entry.lineStart) || 1)),
|
|
4666
|
+
lineEnd: Math.max(Math.max(1, Math.floor(Number(entry.lineStart) || 1)), Math.floor(Number(entry.lineEnd) || Math.max(1, Math.floor(Number(entry.lineStart) || 1)))),
|
|
4667
|
+
selectionStart,
|
|
4668
|
+
selectionEnd,
|
|
4669
|
+
selectedText: typeof entry.selectedText === "string" ? entry.selectedText : "",
|
|
4670
|
+
selectedDisplayText: typeof entry.selectedDisplayText === "string" && entry.selectedDisplayText ? entry.selectedDisplayText : label,
|
|
4671
|
+
};
|
|
4672
|
+
}
|
|
4673
|
+
|
|
4674
|
+
function getOutlineKindLabel(kind) {
|
|
4675
|
+
switch (String(kind || "")) {
|
|
4676
|
+
case "heading": return "Heading";
|
|
4677
|
+
case "section": return "Section";
|
|
4678
|
+
case "subsection": return "Subsection";
|
|
4679
|
+
case "subsubsection": return "Subsubsection";
|
|
4680
|
+
case "paragraph": return "Paragraph";
|
|
4681
|
+
case "subparagraph": return "Subparagraph";
|
|
4682
|
+
case "class": return "Class";
|
|
4683
|
+
case "function": return "Function";
|
|
4684
|
+
case "interface": return "Interface";
|
|
4685
|
+
case "enum": return "Enum";
|
|
4686
|
+
case "type": return "Type";
|
|
4687
|
+
case "struct": return "Struct";
|
|
4688
|
+
case "module": return "Module";
|
|
4689
|
+
case "macro": return "Macro";
|
|
4690
|
+
case "file": return "File";
|
|
4691
|
+
case "hunk": return "Hunk";
|
|
4692
|
+
default: return "Item";
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
|
|
4696
|
+
function getOutlineKindBadge(kind) {
|
|
4697
|
+
switch (String(kind || "")) {
|
|
4698
|
+
case "section": return "§";
|
|
4699
|
+
case "subsection": return "§§";
|
|
4700
|
+
case "subsubsection": return "§3";
|
|
4701
|
+
case "paragraph": return "¶";
|
|
4702
|
+
case "subparagraph": return "¶2";
|
|
4703
|
+
case "class": return "class";
|
|
4704
|
+
case "function": return "def";
|
|
4705
|
+
case "interface": return "iface";
|
|
4706
|
+
case "enum": return "enum";
|
|
4707
|
+
case "type": return "type";
|
|
4708
|
+
case "struct": return "struct";
|
|
4709
|
+
case "module": return "mod";
|
|
4710
|
+
case "macro": return "macro";
|
|
4711
|
+
case "file": return "file";
|
|
4712
|
+
case "hunk": return "@@";
|
|
4713
|
+
default: return "#";
|
|
4714
|
+
}
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4717
|
+
function scanMarkdownOutlineEntries(text) {
|
|
4718
|
+
const { source, lines, lineOffsets } = buildOutlineLineIndex(text);
|
|
4719
|
+
const entries = [];
|
|
4720
|
+
let activeFence = null;
|
|
4721
|
+
|
|
4722
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
4723
|
+
const line = String(lines[lineIndex] || "");
|
|
4724
|
+
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
4725
|
+
if (fenceMatch) {
|
|
4726
|
+
if (!activeFence) {
|
|
4727
|
+
activeFence = fenceMatch[1];
|
|
4728
|
+
} else if (fenceMatch[1][0] === activeFence[0] && fenceMatch[1].length >= activeFence.length) {
|
|
4729
|
+
activeFence = null;
|
|
4730
|
+
}
|
|
4731
|
+
continue;
|
|
4732
|
+
}
|
|
4733
|
+
if (activeFence) continue;
|
|
4734
|
+
|
|
4735
|
+
const atxMatch = line.match(/^ {0,3}(#{1,6})[ \t]+(.+?)(?:[ \t]+#+[ \t]*)?$/);
|
|
4736
|
+
if (atxMatch) {
|
|
4737
|
+
const label = normalizeVisiblePreviewText(atxMatch[2] || "");
|
|
4738
|
+
const entry = makeOutlineEntry({
|
|
4739
|
+
kind: atxMatch[1].length === 1 ? "section" : atxMatch[1].length === 2 ? "subsection" : atxMatch[1].length === 3 ? "subsubsection" : "heading",
|
|
4740
|
+
depth: atxMatch[1].length,
|
|
4741
|
+
label,
|
|
4742
|
+
lineStart: lineIndex + 1,
|
|
4743
|
+
lineEnd: lineIndex + 1,
|
|
4744
|
+
selectionStart: lineOffsets[lineIndex] || 0,
|
|
4745
|
+
selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
|
|
4746
|
+
selectedText: line,
|
|
4747
|
+
selectedDisplayText: label,
|
|
4748
|
+
});
|
|
4749
|
+
if (entry) entries.push(entry);
|
|
4750
|
+
continue;
|
|
4751
|
+
}
|
|
4752
|
+
|
|
4753
|
+
const nextLine = lineIndex + 1 < lines.length ? String(lines[lineIndex + 1] || "") : "";
|
|
4754
|
+
const setextMatch = nextLine.match(/^ {0,3}(=+|-+)\s*$/);
|
|
4755
|
+
if (setextMatch && normalizeVisiblePreviewText(line)) {
|
|
4756
|
+
const depth = setextMatch[1][0] === "=" ? 1 : 2;
|
|
4757
|
+
const label = normalizeVisiblePreviewText(line);
|
|
4758
|
+
const entry = makeOutlineEntry({
|
|
4759
|
+
kind: depth === 1 ? "section" : "subsection",
|
|
4760
|
+
depth,
|
|
4761
|
+
label,
|
|
4762
|
+
lineStart: lineIndex + 1,
|
|
4763
|
+
lineEnd: lineIndex + 1,
|
|
4764
|
+
selectionStart: lineOffsets[lineIndex] || 0,
|
|
4765
|
+
selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
|
|
4766
|
+
selectedText: line,
|
|
4767
|
+
selectedDisplayText: label,
|
|
4768
|
+
});
|
|
4769
|
+
if (entry) entries.push(entry);
|
|
4770
|
+
lineIndex += 1;
|
|
4771
|
+
}
|
|
4772
|
+
}
|
|
4773
|
+
|
|
4774
|
+
return entries;
|
|
4775
|
+
}
|
|
4776
|
+
|
|
4777
|
+
const LATEX_OUTLINE_LEVEL_BY_COMMAND = {
|
|
4778
|
+
part: 1,
|
|
4779
|
+
chapter: 1,
|
|
4780
|
+
section: 1,
|
|
4781
|
+
subsection: 2,
|
|
4782
|
+
subsubsection: 3,
|
|
4783
|
+
paragraph: 4,
|
|
4784
|
+
subparagraph: 5,
|
|
4785
|
+
};
|
|
4786
|
+
|
|
4787
|
+
function scanLatexOutlineEntries(text) {
|
|
4788
|
+
const source = String(text || "").replace(/\r\n/g, "\n");
|
|
4789
|
+
const bodyRange = findLatexDocumentBodyRange(source);
|
|
4790
|
+
const bodyStart = Math.max(0, Math.min(bodyRange.start, source.length));
|
|
4791
|
+
const bodyEnd = Math.max(bodyStart, Math.min(bodyRange.end, source.length));
|
|
4792
|
+
const bodyText = source.slice(bodyStart, bodyEnd);
|
|
4793
|
+
const { lines, lineOffsets } = buildOutlineLineIndex(bodyText);
|
|
4794
|
+
const entries = [];
|
|
4795
|
+
|
|
4796
|
+
function getLine(index) {
|
|
4797
|
+
return index >= 0 && index < lines.length ? String(lines[index] || "") : "";
|
|
4798
|
+
}
|
|
4799
|
+
|
|
4800
|
+
function getStrippedLine(index) {
|
|
4801
|
+
return stripLatexPreviewComments(getLine(index)).trim();
|
|
4802
|
+
}
|
|
4803
|
+
|
|
4804
|
+
function isBibliographyCommandLine(index) {
|
|
4805
|
+
return /^\\(?:bibliographystyle|bibliography|printbibliography)\b/i.test(getStrippedLine(index));
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
4809
|
+
let chunk = getLine(lineIndex);
|
|
4810
|
+
let endLineIndex = lineIndex;
|
|
4811
|
+
let heading = readLatexHeadingChunk(chunk);
|
|
4812
|
+
if (/^\s*\\(?:part|chapter|section|subsection|subsubsection|paragraph|subparagraph)\b/.test(chunk)) {
|
|
4813
|
+
while (!heading && endLineIndex + 1 < lines.length && endLineIndex < lineIndex + 5) {
|
|
4814
|
+
endLineIndex += 1;
|
|
4815
|
+
chunk += "\n" + getLine(endLineIndex);
|
|
4816
|
+
heading = readLatexHeadingChunk(chunk);
|
|
4817
|
+
}
|
|
4818
|
+
}
|
|
4819
|
+
if (heading) {
|
|
4820
|
+
const label = extractLatexPreviewVisibleText(heading.titleText || "");
|
|
4821
|
+
const kind = String(heading.commandName || "section").replace(/\*$/, "").toLowerCase();
|
|
4822
|
+
const entry = makeOutlineEntry({
|
|
4823
|
+
kind,
|
|
4824
|
+
depth: LATEX_OUTLINE_LEVEL_BY_COMMAND[kind] || 1,
|
|
4825
|
+
label,
|
|
4826
|
+
lineStart: lineIndex + 1,
|
|
4827
|
+
lineEnd: endLineIndex + 1,
|
|
4828
|
+
selectionStart: bodyStart + (lineOffsets[lineIndex] || 0),
|
|
4829
|
+
selectionEnd: bodyStart + (lineOffsets[endLineIndex] || 0) + getLine(endLineIndex).length,
|
|
4830
|
+
selectedText: source.slice(bodyStart + (lineOffsets[lineIndex] || 0), bodyStart + (lineOffsets[endLineIndex] || 0) + getLine(endLineIndex).length),
|
|
4831
|
+
selectedDisplayText: label,
|
|
4832
|
+
});
|
|
4833
|
+
if (entry) entries.push(entry);
|
|
4834
|
+
lineIndex = endLineIndex;
|
|
4835
|
+
continue;
|
|
4836
|
+
}
|
|
4837
|
+
|
|
4838
|
+
if (isBibliographyCommandLine(lineIndex)) {
|
|
4839
|
+
let endLine = lineIndex;
|
|
4840
|
+
while (endLine + 1 < lines.length && isBibliographyCommandLine(endLine + 1)) {
|
|
4841
|
+
endLine += 1;
|
|
4842
|
+
}
|
|
4843
|
+
const entry = makeOutlineEntry({
|
|
4844
|
+
kind: "section",
|
|
4845
|
+
depth: 1,
|
|
4846
|
+
label: "References",
|
|
4847
|
+
lineStart: lineIndex + 1,
|
|
4848
|
+
lineEnd: endLine + 1,
|
|
4849
|
+
selectionStart: bodyStart + (lineOffsets[lineIndex] || 0),
|
|
4850
|
+
selectionEnd: bodyStart + (lineOffsets[endLine] || 0) + getLine(endLine).length,
|
|
4851
|
+
selectedText: source.slice(bodyStart + (lineOffsets[lineIndex] || 0), bodyStart + (lineOffsets[endLine] || 0) + getLine(endLine).length),
|
|
4852
|
+
selectedDisplayText: "References",
|
|
4853
|
+
});
|
|
4854
|
+
if (entry) entries.push(entry);
|
|
4855
|
+
lineIndex = endLine;
|
|
4856
|
+
}
|
|
4857
|
+
}
|
|
4858
|
+
|
|
4859
|
+
return entries;
|
|
4860
|
+
}
|
|
4861
|
+
|
|
4862
|
+
function scanPythonOutlineEntries(text) {
|
|
4863
|
+
const { lines, lineOffsets } = buildOutlineLineIndex(text);
|
|
4864
|
+
const entries = [];
|
|
4865
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
4866
|
+
const line = String(lines[lineIndex] || "");
|
|
4867
|
+
const classMatch = line.match(/^(\s*)class\s+([A-Za-z_][A-Za-z0-9_]*)\b/);
|
|
4868
|
+
const defMatch = line.match(/^(\s*)(?:async\s+def|def)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/);
|
|
4869
|
+
const match = classMatch || defMatch;
|
|
4870
|
+
if (!match) continue;
|
|
4871
|
+
const indent = String(match[1] || "").replace(/\t/g, " ").length;
|
|
4872
|
+
const label = String(match[2] || "");
|
|
4873
|
+
const kind = classMatch ? "class" : "function";
|
|
4874
|
+
const entry = makeOutlineEntry({
|
|
4875
|
+
kind,
|
|
4876
|
+
depth: Math.max(1, Math.floor(indent / 4) + 1),
|
|
4877
|
+
label,
|
|
4878
|
+
lineStart: lineIndex + 1,
|
|
4879
|
+
lineEnd: lineIndex + 1,
|
|
4880
|
+
selectionStart: lineOffsets[lineIndex] || 0,
|
|
4881
|
+
selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
|
|
4882
|
+
selectedText: line,
|
|
4883
|
+
selectedDisplayText: label,
|
|
4884
|
+
});
|
|
4885
|
+
if (entry) entries.push(entry);
|
|
4886
|
+
}
|
|
4887
|
+
return entries;
|
|
4888
|
+
}
|
|
4889
|
+
|
|
4890
|
+
function scanJsLikeOutlineEntries(text) {
|
|
4891
|
+
const { lines, lineOffsets } = buildOutlineLineIndex(text);
|
|
4892
|
+
const entries = [];
|
|
4893
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
4894
|
+
const line = String(lines[lineIndex] || "");
|
|
4895
|
+
const patterns = [
|
|
4896
|
+
{ kind: "class", match: line.match(/^(\s*)(?:export\s+)?(?:default\s+)?class\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/) },
|
|
4897
|
+
{ kind: "function", match: line.match(/^(\s*)(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/) },
|
|
4898
|
+
{ kind: "function", match: line.match(/^(\s*)(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?(?:\([^)]*\)|[A-Za-z_$][A-Za-z0-9_$]*)\s*=>/) },
|
|
4899
|
+
{ kind: "interface", match: line.match(/^(\s*)(?:export\s+)?interface\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/) },
|
|
4900
|
+
{ kind: "enum", match: line.match(/^(\s*)(?:export\s+)?enum\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/) },
|
|
4901
|
+
{ kind: "type", match: line.match(/^(\s*)(?:export\s+)?type\s+([A-Za-z_$][A-Za-z0-9_$]*)\b/) },
|
|
4902
|
+
];
|
|
4903
|
+
const found = patterns.find((entry) => entry.match);
|
|
4904
|
+
if (!found || !found.match) continue;
|
|
4905
|
+
const indent = String(found.match[1] || "").replace(/\t/g, " ").length;
|
|
4906
|
+
const label = String(found.match[2] || "");
|
|
4907
|
+
const entry = makeOutlineEntry({
|
|
4908
|
+
kind: found.kind,
|
|
4909
|
+
depth: Math.max(1, Math.floor(indent / 2) + 1),
|
|
4910
|
+
label,
|
|
4911
|
+
lineStart: lineIndex + 1,
|
|
4912
|
+
lineEnd: lineIndex + 1,
|
|
4913
|
+
selectionStart: lineOffsets[lineIndex] || 0,
|
|
4914
|
+
selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
|
|
4915
|
+
selectedText: line,
|
|
4916
|
+
selectedDisplayText: label,
|
|
4917
|
+
});
|
|
4918
|
+
if (entry) entries.push(entry);
|
|
4919
|
+
}
|
|
4920
|
+
return entries;
|
|
4921
|
+
}
|
|
4922
|
+
|
|
4923
|
+
function scanJuliaOutlineEntries(text) {
|
|
4924
|
+
const { lines, lineOffsets } = buildOutlineLineIndex(text);
|
|
4925
|
+
const entries = [];
|
|
4926
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
4927
|
+
const line = String(lines[lineIndex] || "");
|
|
4928
|
+
const patterns = [
|
|
4929
|
+
{ kind: "module", match: line.match(/^(\s*)module\s+([A-Za-z_][A-Za-z0-9_]*)\b/) },
|
|
4930
|
+
{ kind: "struct", match: line.match(/^(\s*)(?:mutable\s+)?struct\s+([A-Za-z_][A-Za-z0-9_]*)\b/) },
|
|
4931
|
+
{ kind: "function", match: line.match(/^(\s*)function\s+([A-Za-z_][A-Za-z0-9_!]*)\s*\(/) },
|
|
4932
|
+
{ kind: "macro", match: line.match(/^(\s*)macro\s+([A-Za-z_][A-Za-z0-9_!]*)\b/) },
|
|
4933
|
+
];
|
|
4934
|
+
const found = patterns.find((entry) => entry.match);
|
|
4935
|
+
if (!found || !found.match) continue;
|
|
4936
|
+
const indent = String(found.match[1] || "").replace(/\t/g, " ").length;
|
|
4937
|
+
const label = String(found.match[2] || "");
|
|
4938
|
+
const entry = makeOutlineEntry({
|
|
4939
|
+
kind: found.kind,
|
|
4940
|
+
depth: Math.max(1, Math.floor(indent / 2) + 1),
|
|
4941
|
+
label,
|
|
4942
|
+
lineStart: lineIndex + 1,
|
|
4943
|
+
lineEnd: lineIndex + 1,
|
|
4944
|
+
selectionStart: lineOffsets[lineIndex] || 0,
|
|
4945
|
+
selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
|
|
4946
|
+
selectedText: line,
|
|
4947
|
+
selectedDisplayText: label,
|
|
4948
|
+
});
|
|
4949
|
+
if (entry) entries.push(entry);
|
|
4950
|
+
}
|
|
4951
|
+
return entries;
|
|
4952
|
+
}
|
|
4953
|
+
|
|
4954
|
+
function scanBashOutlineEntries(text) {
|
|
4955
|
+
const { lines, lineOffsets } = buildOutlineLineIndex(text);
|
|
4956
|
+
const entries = [];
|
|
4957
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
4958
|
+
const line = String(lines[lineIndex] || "");
|
|
4959
|
+
const match = line.match(/^(\s*)(?:function\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*\(\)\s*\{/);
|
|
4960
|
+
if (!match) continue;
|
|
4961
|
+
const indent = String(match[1] || "").replace(/\t/g, " ").length;
|
|
4962
|
+
const label = String(match[2] || "");
|
|
4963
|
+
const entry = makeOutlineEntry({
|
|
4964
|
+
kind: "function",
|
|
4965
|
+
depth: Math.max(1, Math.floor(indent / 2) + 1),
|
|
4966
|
+
label,
|
|
4967
|
+
lineStart: lineIndex + 1,
|
|
4968
|
+
lineEnd: lineIndex + 1,
|
|
4969
|
+
selectionStart: lineOffsets[lineIndex] || 0,
|
|
4970
|
+
selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
|
|
4971
|
+
selectedText: line,
|
|
4972
|
+
selectedDisplayText: label,
|
|
4973
|
+
});
|
|
4974
|
+
if (entry) entries.push(entry);
|
|
4975
|
+
}
|
|
4976
|
+
return entries;
|
|
4977
|
+
}
|
|
4978
|
+
|
|
4979
|
+
function scanDiffOutlineEntries(text) {
|
|
4980
|
+
const { lines, lineOffsets } = buildOutlineLineIndex(text);
|
|
4981
|
+
const entries = [];
|
|
4982
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
4983
|
+
const line = String(lines[lineIndex] || "");
|
|
4984
|
+
let kind = "";
|
|
4985
|
+
let label = "";
|
|
4986
|
+
let depth = 1;
|
|
4987
|
+
const fileMatch = line.match(/^diff\s+--git\s+a\/([^\s]+)\s+b\/([^\s]+)/);
|
|
4988
|
+
if (fileMatch) {
|
|
4989
|
+
kind = "file";
|
|
4990
|
+
label = String(fileMatch[2] || fileMatch[1] || "");
|
|
4991
|
+
depth = 1;
|
|
4992
|
+
} else if (/^@@/.test(line)) {
|
|
4993
|
+
kind = "hunk";
|
|
4994
|
+
label = line.replace(/^@@\s*|\s*@@.*$/g, "").trim() || line.trim();
|
|
4995
|
+
depth = 2;
|
|
4996
|
+
}
|
|
4997
|
+
if (!kind || !label) continue;
|
|
4998
|
+
const entry = makeOutlineEntry({
|
|
4999
|
+
kind,
|
|
5000
|
+
depth,
|
|
5001
|
+
label,
|
|
5002
|
+
lineStart: lineIndex + 1,
|
|
5003
|
+
lineEnd: lineIndex + 1,
|
|
5004
|
+
selectionStart: lineOffsets[lineIndex] || 0,
|
|
5005
|
+
selectionEnd: (lineOffsets[lineIndex] || 0) + line.length,
|
|
5006
|
+
selectedText: line,
|
|
5007
|
+
selectedDisplayText: label,
|
|
5008
|
+
});
|
|
5009
|
+
if (entry) entries.push(entry);
|
|
5010
|
+
}
|
|
5011
|
+
return entries;
|
|
5012
|
+
}
|
|
5013
|
+
|
|
5014
|
+
function scanOutlineEntries(text, language) {
|
|
5015
|
+
switch (String(language || "").toLowerCase()) {
|
|
5016
|
+
case "markdown":
|
|
5017
|
+
return scanMarkdownOutlineEntries(text);
|
|
5018
|
+
case "latex":
|
|
5019
|
+
return scanLatexOutlineEntries(text);
|
|
5020
|
+
case "python":
|
|
5021
|
+
return scanPythonOutlineEntries(text);
|
|
5022
|
+
case "javascript":
|
|
5023
|
+
case "typescript":
|
|
5024
|
+
return scanJsLikeOutlineEntries(text);
|
|
5025
|
+
case "julia":
|
|
5026
|
+
return scanJuliaOutlineEntries(text);
|
|
5027
|
+
case "bash":
|
|
5028
|
+
return scanBashOutlineEntries(text);
|
|
5029
|
+
case "diff":
|
|
5030
|
+
return scanDiffOutlineEntries(text);
|
|
5031
|
+
default:
|
|
5032
|
+
return [];
|
|
5033
|
+
}
|
|
5034
|
+
}
|
|
5035
|
+
|
|
4201
5036
|
function cloneReviewNotes(notes) {
|
|
4202
5037
|
return Array.isArray(notes)
|
|
4203
5038
|
? notes
|
|
@@ -7011,6 +7846,138 @@
|
|
|
7011
7846
|
updateEditorSelectionCommentUi();
|
|
7012
7847
|
}
|
|
7013
7848
|
|
|
7849
|
+
function getOutlineEntriesForCurrentEditor() {
|
|
7850
|
+
return scanOutlineEntries(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "", editorLanguage || "markdown");
|
|
7851
|
+
}
|
|
7852
|
+
|
|
7853
|
+
function updateOutlineUi() {
|
|
7854
|
+
outlineEntries = getOutlineEntriesForCurrentEditor();
|
|
7855
|
+
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
7856
|
+
const count = outlineEntries.length;
|
|
7857
|
+
const hasEntries = count > 0;
|
|
7858
|
+
const isOpen = isOutlineOpen();
|
|
7859
|
+
if (outlineBtn) {
|
|
7860
|
+
outlineBtn.textContent = "Outline";
|
|
7861
|
+
outlineBtn.classList.remove("has-content");
|
|
7862
|
+
outlineBtn.classList.toggle("is-active", isOpen);
|
|
7863
|
+
outlineBtn.setAttribute("aria-pressed", isOpen ? "true" : "false");
|
|
7864
|
+
outlineBtn.title = isOpen
|
|
7865
|
+
? "Hide document outline."
|
|
7866
|
+
: (hasEntries
|
|
7867
|
+
? (count + " outline entr" + (count === 1 ? "y" : "ies") + " for " + descriptor.label + ". Open the outline rail.")
|
|
7868
|
+
: "Open document outline for the current editor text.");
|
|
7869
|
+
}
|
|
7870
|
+
if (outlineMetaEl) {
|
|
7871
|
+
outlineMetaEl.textContent = hasEntries
|
|
7872
|
+
? (count + " entr" + (count === 1 ? "y" : "ies") + " · " + (editorLanguage || "text") + " · " + descriptor.label)
|
|
7873
|
+
: ("No outline entries · " + (editorLanguage || "text"));
|
|
7874
|
+
}
|
|
7875
|
+
if (outlineDoneBtn) {
|
|
7876
|
+
outlineDoneBtn.disabled = !isOpen;
|
|
7877
|
+
}
|
|
7878
|
+
if (outlineEmptyStateEl) {
|
|
7879
|
+
outlineEmptyStateEl.hidden = hasEntries;
|
|
7880
|
+
}
|
|
7881
|
+
renderOutlineList();
|
|
7882
|
+
}
|
|
7883
|
+
|
|
7884
|
+
function renderOutlineList() {
|
|
7885
|
+
if (!outlineListEl) return;
|
|
7886
|
+
outlineListEl.innerHTML = "";
|
|
7887
|
+
for (const entry of outlineEntries) {
|
|
7888
|
+
const itemBtn = document.createElement("button");
|
|
7889
|
+
itemBtn.type = "button";
|
|
7890
|
+
itemBtn.className = "outline-entry";
|
|
7891
|
+
itemBtn.dataset.outlineId = String(entry.id || "");
|
|
7892
|
+
itemBtn.style.paddingLeft = (10 + Math.max(0, (entry.depth || 1) - 1) * 14) + "px";
|
|
7893
|
+
itemBtn.title = getOutlineKindLabel(entry.kind) + " · line " + String(entry.lineStart || 1) + "\n" + String(entry.label || "");
|
|
7894
|
+
|
|
7895
|
+
const kindEl = document.createElement("span");
|
|
7896
|
+
kindEl.className = "outline-entry-kind";
|
|
7897
|
+
kindEl.textContent = getOutlineKindBadge(entry.kind);
|
|
7898
|
+
itemBtn.appendChild(kindEl);
|
|
7899
|
+
|
|
7900
|
+
const titleEl = document.createElement("span");
|
|
7901
|
+
titleEl.className = "outline-entry-title";
|
|
7902
|
+
titleEl.textContent = String(entry.label || "");
|
|
7903
|
+
itemBtn.appendChild(titleEl);
|
|
7904
|
+
|
|
7905
|
+
const metaEl = document.createElement("span");
|
|
7906
|
+
metaEl.className = "outline-entry-meta";
|
|
7907
|
+
metaEl.textContent = "L" + String(entry.lineStart || 1);
|
|
7908
|
+
itemBtn.appendChild(metaEl);
|
|
7909
|
+
|
|
7910
|
+
outlineListEl.appendChild(itemBtn);
|
|
7911
|
+
}
|
|
7912
|
+
}
|
|
7913
|
+
|
|
7914
|
+
function buildOutlineEntryAnchor(entry) {
|
|
7915
|
+
if (!entry) return null;
|
|
7916
|
+
return normalizeReviewNote({
|
|
7917
|
+
selectionStart: entry.selectionStart,
|
|
7918
|
+
selectionEnd: entry.selectionEnd,
|
|
7919
|
+
lineStart: entry.lineStart,
|
|
7920
|
+
lineEnd: entry.lineEnd,
|
|
7921
|
+
selectedText: entry.selectedText,
|
|
7922
|
+
selectedDisplayText: entry.selectedDisplayText || entry.label,
|
|
7923
|
+
});
|
|
7924
|
+
}
|
|
7925
|
+
|
|
7926
|
+
function jumpToOutlineEntry(entryId) {
|
|
7927
|
+
const entry = outlineEntries.find((candidate) => candidate && String(candidate.id || "") === String(entryId || ""));
|
|
7928
|
+
if (!entry) return false;
|
|
7929
|
+
const anchor = buildOutlineEntryAnchor(entry);
|
|
7930
|
+
if (!anchor) return false;
|
|
7931
|
+
return jumpToReviewAnchor(anchor, {
|
|
7932
|
+
statusMessage: "Jumped to outline entry.",
|
|
7933
|
+
afterJump: () => {
|
|
7934
|
+
revealReviewNoteInPreview(anchor);
|
|
7935
|
+
},
|
|
7936
|
+
});
|
|
7937
|
+
}
|
|
7938
|
+
|
|
7939
|
+
function closeOutline(options) {
|
|
7940
|
+
if (!outlineOverlayEl || outlineOverlayEl.hidden) return;
|
|
7941
|
+
outlineOverlayEl.hidden = true;
|
|
7942
|
+
updateOutlineUi();
|
|
7943
|
+
if (editorView === "markdown") {
|
|
7944
|
+
scheduleEditorLineNumberRender();
|
|
7945
|
+
}
|
|
7946
|
+
const focusTarget = options && Object.prototype.hasOwnProperty.call(options, "focusTarget")
|
|
7947
|
+
? options.focusTarget
|
|
7948
|
+
: (outlineReturnFocusEl || outlineBtn || sourceTextEl);
|
|
7949
|
+
outlineReturnFocusEl = null;
|
|
7950
|
+
if (focusTarget && typeof focusTarget.focus === "function") {
|
|
7951
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
7952
|
+
? window.requestAnimationFrame.bind(window)
|
|
7953
|
+
: (cb) => window.setTimeout(cb, 16);
|
|
7954
|
+
schedule(() => focusTarget.focus());
|
|
7955
|
+
}
|
|
7956
|
+
}
|
|
7957
|
+
|
|
7958
|
+
function openOutline() {
|
|
7959
|
+
if (!outlineOverlayEl) return;
|
|
7960
|
+
if (isReviewNotesOpen()) {
|
|
7961
|
+
closeReviewNotes({ focusTarget: null });
|
|
7962
|
+
}
|
|
7963
|
+
outlineReturnFocusEl = document.activeElement && document.activeElement !== document.body
|
|
7964
|
+
? document.activeElement
|
|
7965
|
+
: sourceTextEl;
|
|
7966
|
+
outlineOverlayEl.hidden = false;
|
|
7967
|
+
updateOutlineUi();
|
|
7968
|
+
if (editorView === "markdown") {
|
|
7969
|
+
scheduleEditorLineNumberRender();
|
|
7970
|
+
}
|
|
7971
|
+
}
|
|
7972
|
+
|
|
7973
|
+
function toggleOutline() {
|
|
7974
|
+
if (isOutlineOpen()) {
|
|
7975
|
+
closeOutline({ focusTarget: outlineBtn || sourceTextEl });
|
|
7976
|
+
} else {
|
|
7977
|
+
openOutline();
|
|
7978
|
+
}
|
|
7979
|
+
}
|
|
7980
|
+
|
|
7014
7981
|
function updateReviewNotesUi() {
|
|
7015
7982
|
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
7016
7983
|
const count = reviewNotes.length;
|
|
@@ -7382,6 +8349,18 @@
|
|
|
7382
8349
|
selection.removeAllRanges();
|
|
7383
8350
|
}
|
|
7384
8351
|
clearPreviewCommentSelection();
|
|
8352
|
+
const current = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
|
|
8353
|
+
const range = resolveReviewNoteRange(previewNote, current);
|
|
8354
|
+
if (range && sourceTextEl) {
|
|
8355
|
+
try {
|
|
8356
|
+
sourceTextEl.focus({ preventScroll: true });
|
|
8357
|
+
} catch {
|
|
8358
|
+
sourceTextEl.focus();
|
|
8359
|
+
}
|
|
8360
|
+
if (typeof sourceTextEl.setSelectionRange === "function") {
|
|
8361
|
+
sourceTextEl.setSelectionRange(range.start, range.end);
|
|
8362
|
+
}
|
|
8363
|
+
}
|
|
7385
8364
|
});
|
|
7386
8365
|
},
|
|
7387
8366
|
});
|
|
@@ -7537,6 +8516,9 @@
|
|
|
7537
8516
|
if (isReviewNotesOpen()) {
|
|
7538
8517
|
closeReviewNotes({ focusTarget: null });
|
|
7539
8518
|
}
|
|
8519
|
+
if (isOutlineOpen()) {
|
|
8520
|
+
closeOutline({ focusTarget: null });
|
|
8521
|
+
}
|
|
7540
8522
|
scratchpadReturnFocusEl = document.activeElement && document.activeElement !== document.body
|
|
7541
8523
|
? document.activeElement
|
|
7542
8524
|
: sourceTextEl;
|
|
@@ -7580,6 +8562,9 @@
|
|
|
7580
8562
|
if (isScratchpadOpen()) {
|
|
7581
8563
|
closeScratchpad({ focusTarget: null });
|
|
7582
8564
|
}
|
|
8565
|
+
if (isOutlineOpen()) {
|
|
8566
|
+
closeOutline({ focusTarget: null });
|
|
8567
|
+
}
|
|
7583
8568
|
reviewNotesReturnFocusEl = document.activeElement && document.activeElement !== document.body
|
|
7584
8569
|
? document.activeElement
|
|
7585
8570
|
: sourceTextEl;
|
|
@@ -7697,6 +8682,7 @@
|
|
|
7697
8682
|
if (editorView === "preview") {
|
|
7698
8683
|
scheduleSourcePreviewRender(0);
|
|
7699
8684
|
}
|
|
8685
|
+
updateOutlineUi();
|
|
7700
8686
|
}
|
|
7701
8687
|
|
|
7702
8688
|
function setEditorHighlightMode(mode) {
|
|
@@ -8007,6 +8993,10 @@
|
|
|
8007
8993
|
}
|
|
8008
8994
|
}
|
|
8009
8995
|
|
|
8996
|
+
if (message.traceState) {
|
|
8997
|
+
replaceTraceState(message.traceState);
|
|
8998
|
+
}
|
|
8999
|
+
|
|
8010
9000
|
let appliedHistory = false;
|
|
8011
9001
|
if (Array.isArray(message.responseHistory)) {
|
|
8012
9002
|
appliedHistory = setResponseHistory(message.responseHistory, {
|
|
@@ -8053,6 +9043,26 @@
|
|
|
8053
9043
|
return;
|
|
8054
9044
|
}
|
|
8055
9045
|
|
|
9046
|
+
if (message.type === "trace_reset") {
|
|
9047
|
+
replaceTraceState(message.trace);
|
|
9048
|
+
return;
|
|
9049
|
+
}
|
|
9050
|
+
|
|
9051
|
+
if (message.type === "trace_status") {
|
|
9052
|
+
updateTraceStatusFromMessage(message);
|
|
9053
|
+
return;
|
|
9054
|
+
}
|
|
9055
|
+
|
|
9056
|
+
if (message.type === "trace_entry_upsert") {
|
|
9057
|
+
upsertTraceEntry(message.entry);
|
|
9058
|
+
return;
|
|
9059
|
+
}
|
|
9060
|
+
|
|
9061
|
+
if (message.type === "trace_assistant_delta") {
|
|
9062
|
+
appendTraceAssistantDelta(message.entryId, message.deltaKind, message.delta, message.updatedAt);
|
|
9063
|
+
return;
|
|
9064
|
+
}
|
|
9065
|
+
|
|
8056
9066
|
if (message.type === "request_started") {
|
|
8057
9067
|
pendingRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
|
|
8058
9068
|
pendingKind = typeof message.kind === "string" ? message.kind : "unknown";
|
|
@@ -8750,6 +9760,8 @@
|
|
|
8750
9760
|
setRightView(rightViewSelect.value);
|
|
8751
9761
|
});
|
|
8752
9762
|
|
|
9763
|
+
attachResponsePaneInteractionHandlers();
|
|
9764
|
+
|
|
8753
9765
|
followSelect.addEventListener("change", () => {
|
|
8754
9766
|
followLatest = followSelect.value !== "off";
|
|
8755
9767
|
if (followLatest && queuedLatestResponse) {
|
|
@@ -8896,6 +9908,7 @@
|
|
|
8896
9908
|
renderSourcePreview({ previewDelayMs: PREVIEW_INPUT_DEBOUNCE_MS });
|
|
8897
9909
|
scheduleEditorMetaUpdate();
|
|
8898
9910
|
updateEditorSelectionCommentUi();
|
|
9911
|
+
updateOutlineUi();
|
|
8899
9912
|
if (isReviewNotesOpen() && reviewNotes.length > 0) {
|
|
8900
9913
|
renderReviewNotesList();
|
|
8901
9914
|
updateReviewNotesUi();
|
|
@@ -8991,17 +10004,6 @@
|
|
|
8991
10004
|
});
|
|
8992
10005
|
|
|
8993
10006
|
loadResponseBtn.addEventListener("click", () => {
|
|
8994
|
-
if (rightView === "thinking") {
|
|
8995
|
-
if (!latestResponseThinking.trim()) {
|
|
8996
|
-
setStatus("No thinking available for the selected response.", "warning");
|
|
8997
|
-
return;
|
|
8998
|
-
}
|
|
8999
|
-
setEditorText(latestResponseThinking, { preserveScroll: false, preserveSelection: false });
|
|
9000
|
-
setSourceState({ source: "blank", label: "assistant thinking", path: null });
|
|
9001
|
-
setStatus("Loaded thinking into editor.", "success");
|
|
9002
|
-
return;
|
|
9003
|
-
}
|
|
9004
|
-
|
|
9005
10007
|
if (!latestResponseMarkdown.trim()) {
|
|
9006
10008
|
setStatus("No response available yet.", "warning");
|
|
9007
10009
|
return;
|
|
@@ -9040,15 +10042,15 @@
|
|
|
9040
10042
|
});
|
|
9041
10043
|
|
|
9042
10044
|
copyResponseBtn.addEventListener("click", async () => {
|
|
9043
|
-
const content =
|
|
10045
|
+
const content = latestResponseMarkdown;
|
|
9044
10046
|
if (!content.trim()) {
|
|
9045
|
-
setStatus(
|
|
10047
|
+
setStatus("No response available yet.", "warning");
|
|
9046
10048
|
return;
|
|
9047
10049
|
}
|
|
9048
10050
|
|
|
9049
10051
|
try {
|
|
9050
10052
|
await navigator.clipboard.writeText(content);
|
|
9051
|
-
setStatus(
|
|
10053
|
+
setStatus("Copied response text.", "success");
|
|
9052
10054
|
} catch (error) {
|
|
9053
10055
|
setStatus("Clipboard write failed.", "warning");
|
|
9054
10056
|
}
|
|
@@ -9284,6 +10286,35 @@
|
|
|
9284
10286
|
});
|
|
9285
10287
|
}
|
|
9286
10288
|
|
|
10289
|
+
if (outlineBtn) {
|
|
10290
|
+
outlineBtn.addEventListener("click", () => {
|
|
10291
|
+
toggleOutline();
|
|
10292
|
+
});
|
|
10293
|
+
}
|
|
10294
|
+
|
|
10295
|
+
if (outlineCloseBtn) {
|
|
10296
|
+
outlineCloseBtn.addEventListener("click", () => {
|
|
10297
|
+
closeOutline();
|
|
10298
|
+
});
|
|
10299
|
+
}
|
|
10300
|
+
|
|
10301
|
+
if (outlineDoneBtn) {
|
|
10302
|
+
outlineDoneBtn.addEventListener("click", () => {
|
|
10303
|
+
closeOutline();
|
|
10304
|
+
});
|
|
10305
|
+
}
|
|
10306
|
+
|
|
10307
|
+
if (outlineListEl) {
|
|
10308
|
+
outlineListEl.addEventListener("click", (event) => {
|
|
10309
|
+
const target = event.target;
|
|
10310
|
+
const entryBtn = target instanceof Element ? target.closest(".outline-entry") : null;
|
|
10311
|
+
if (!entryBtn) return;
|
|
10312
|
+
const outlineId = entryBtn.getAttribute("data-outline-id") || "";
|
|
10313
|
+
if (!outlineId) return;
|
|
10314
|
+
jumpToOutlineEntry(outlineId);
|
|
10315
|
+
});
|
|
10316
|
+
}
|
|
10317
|
+
|
|
9287
10318
|
if (reviewNotesCloseBtn) {
|
|
9288
10319
|
reviewNotesCloseBtn.addEventListener("click", () => {
|
|
9289
10320
|
closeReviewNotes();
|