pi-studio 0.5.52 → 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 +15 -0
- package/README.md +3 -3
- package/client/studio-client.js +513 -85
- package/client/studio.css +176 -0
- package/index.ts +332 -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");
|
|
@@ -179,6 +183,10 @@
|
|
|
179
183
|
let latestCritiqueNotesNormalized = "";
|
|
180
184
|
let responseHistory = [];
|
|
181
185
|
let responseHistoryIndex = -1;
|
|
186
|
+
let traceState = null;
|
|
187
|
+
let traceFilter = "all";
|
|
188
|
+
let traceAutoScroll = true;
|
|
189
|
+
let traceRenderRaf = null;
|
|
182
190
|
let studioRunChainActive = false;
|
|
183
191
|
let queuedSteeringCount = 0;
|
|
184
192
|
let agentBusyFromServer = false;
|
|
@@ -231,6 +239,262 @@
|
|
|
231
239
|
return changed;
|
|
232
240
|
}
|
|
233
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
|
+
|
|
234
498
|
contextTokens = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextTokens : null);
|
|
235
499
|
contextWindow = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextWindow : null);
|
|
236
500
|
contextPercent = parseFiniteNumber(document.body && document.body.dataset ? document.body.dataset.contextPercent : null);
|
|
@@ -412,6 +676,17 @@
|
|
|
412
676
|
if (typeof message.label === "string") summary.label = message.label;
|
|
413
677
|
if (Array.isArray(message.responseHistory)) summary.responseHistoryCount = message.responseHistory.length;
|
|
414
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;
|
|
415
690
|
if (typeof message.details === "object" && message.details !== null) summary.details = message.details;
|
|
416
691
|
return summary;
|
|
417
692
|
}
|
|
@@ -1419,6 +1694,21 @@
|
|
|
1419
1694
|
function updateReferenceBadge() {
|
|
1420
1695
|
if (!referenceBadgeEl) return;
|
|
1421
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
|
+
|
|
1422
1712
|
if (rightView === "editor-preview") {
|
|
1423
1713
|
const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
|
|
1424
1714
|
if (hasResponse) {
|
|
@@ -1432,26 +1722,6 @@
|
|
|
1432
1722
|
}
|
|
1433
1723
|
|
|
1434
1724
|
const hasResponse = Boolean(latestResponseMarkdown && latestResponseMarkdown.trim());
|
|
1435
|
-
const hasThinking = Boolean(latestResponseThinking && latestResponseThinking.trim());
|
|
1436
|
-
if (rightView === "thinking") {
|
|
1437
|
-
if (!hasResponse && !hasThinking) {
|
|
1438
|
-
referenceBadgeEl.textContent = "Thinking: none";
|
|
1439
|
-
return;
|
|
1440
|
-
}
|
|
1441
|
-
|
|
1442
|
-
const time = formatReferenceTime(latestResponseTimestamp);
|
|
1443
|
-
const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
|
|
1444
|
-
const selected = total > 0 && responseHistoryIndex >= 0 && responseHistoryIndex < total
|
|
1445
|
-
? responseHistoryIndex + 1
|
|
1446
|
-
: 0;
|
|
1447
|
-
const historyPrefix = total > 0 ? "Response history " + selected + "/" + total + " · " : "";
|
|
1448
|
-
const thinkingLabel = hasThinking ? "assistant thinking" : "assistant thinking unavailable";
|
|
1449
|
-
referenceBadgeEl.textContent = time
|
|
1450
|
-
? historyPrefix + thinkingLabel + " · " + time
|
|
1451
|
-
: historyPrefix + thinkingLabel;
|
|
1452
|
-
return;
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
1725
|
if (!hasResponse) {
|
|
1456
1726
|
referenceBadgeEl.textContent = "Latest response: none";
|
|
1457
1727
|
return;
|
|
@@ -1527,14 +1797,15 @@
|
|
|
1527
1797
|
function updateSyncBadge(normalizedEditorText) {
|
|
1528
1798
|
if (!syncBadgeEl) return;
|
|
1529
1799
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1800
|
+
if (rightView === "trace") {
|
|
1801
|
+
syncBadgeEl.hidden = true;
|
|
1802
|
+
syncBadgeEl.classList.remove("sync");
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1534
1805
|
|
|
1535
|
-
if (!
|
|
1806
|
+
if (!latestResponseHasContent) {
|
|
1536
1807
|
syncBadgeEl.hidden = true;
|
|
1537
|
-
syncBadgeEl.textContent =
|
|
1808
|
+
syncBadgeEl.textContent = "In sync with response";
|
|
1538
1809
|
syncBadgeEl.classList.remove("sync");
|
|
1539
1810
|
return;
|
|
1540
1811
|
}
|
|
@@ -1542,10 +1813,9 @@
|
|
|
1542
1813
|
const normalizedEditor = typeof normalizedEditorText === "string"
|
|
1543
1814
|
? normalizedEditorText
|
|
1544
1815
|
: normalizeForCompare(sourceTextEl.value);
|
|
1545
|
-
const
|
|
1546
|
-
const inSync = normalizedEditor === targetNormalized;
|
|
1816
|
+
const inSync = normalizedEditor === latestResponseNormalized;
|
|
1547
1817
|
syncBadgeEl.hidden = !inSync;
|
|
1548
|
-
syncBadgeEl.textContent =
|
|
1818
|
+
syncBadgeEl.textContent = "In sync with response";
|
|
1549
1819
|
|
|
1550
1820
|
if (inSync) {
|
|
1551
1821
|
syncBadgeEl.classList.add("sync");
|
|
@@ -2194,6 +2464,40 @@
|
|
|
2194
2464
|
});
|
|
2195
2465
|
}
|
|
2196
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
|
+
|
|
2197
2501
|
function replaceResponsePaneWithClone() {
|
|
2198
2502
|
const currentEl = critiqueViewEl;
|
|
2199
2503
|
if (!currentEl || !currentEl.parentNode || typeof currentEl.cloneNode !== "function") {
|
|
@@ -2207,6 +2511,7 @@
|
|
|
2207
2511
|
|
|
2208
2512
|
currentEl.parentNode.replaceChild(replacement, currentEl);
|
|
2209
2513
|
critiqueViewEl = replacement;
|
|
2514
|
+
attachResponsePaneInteractionHandlers();
|
|
2210
2515
|
return critiqueViewEl;
|
|
2211
2516
|
}
|
|
2212
2517
|
|
|
@@ -2702,7 +3007,134 @@
|
|
|
2702
3007
|
}, delay);
|
|
2703
3008
|
}
|
|
2704
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
|
+
|
|
2705
3132
|
function renderActiveResult() {
|
|
3133
|
+
if (rightView === "trace") {
|
|
3134
|
+
renderTraceView();
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
|
|
2706
3138
|
if (rightView === "editor-preview") {
|
|
2707
3139
|
const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
|
|
2708
3140
|
if (!editorText.trim()) {
|
|
@@ -2721,17 +3153,6 @@
|
|
|
2721
3153
|
return;
|
|
2722
3154
|
}
|
|
2723
3155
|
|
|
2724
|
-
if (rightView === "thinking") {
|
|
2725
|
-
const thinking = latestResponseThinking;
|
|
2726
|
-
finishPreviewRender(critiqueViewEl);
|
|
2727
|
-
critiqueViewEl.innerHTML = thinking && thinking.trim()
|
|
2728
|
-
? buildPlainMarkdownHtml(thinking)
|
|
2729
|
-
: "<pre class='plain-markdown'>No thinking available for this response.</pre>";
|
|
2730
|
-
applyPendingResponseScrollReset();
|
|
2731
|
-
scheduleResponsePaneRepaintNudge();
|
|
2732
|
-
return;
|
|
2733
|
-
}
|
|
2734
|
-
|
|
2735
3156
|
const markdown = latestResponseMarkdown;
|
|
2736
3157
|
if (!markdown || !markdown.trim()) {
|
|
2737
3158
|
finishPreviewRender(critiqueViewEl);
|
|
@@ -2775,55 +3196,43 @@
|
|
|
2775
3196
|
|
|
2776
3197
|
function updateResultActionButtons(normalizedEditorText) {
|
|
2777
3198
|
const hasResponse = latestResponseHasContent;
|
|
2778
|
-
const hasThinking = Boolean(latestResponseThinking && latestResponseThinking.trim());
|
|
2779
3199
|
const normalizedEditor = typeof normalizedEditorText === "string"
|
|
2780
3200
|
? normalizedEditorText
|
|
2781
3201
|
: normalizeForCompare(sourceTextEl.value);
|
|
2782
3202
|
const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
|
|
2783
|
-
const thinkingLoaded = hasThinking && normalizedEditor === latestResponseThinkingNormalized;
|
|
2784
3203
|
const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
|
|
2785
|
-
const
|
|
3204
|
+
const showingTrace = rightView === "trace";
|
|
3205
|
+
|
|
3206
|
+
if (responseWrapEl) {
|
|
3207
|
+
responseWrapEl.hidden = showingTrace;
|
|
3208
|
+
}
|
|
2786
3209
|
|
|
2787
3210
|
const critiqueNotes = isCritiqueResponse ? latestCritiqueNotes : "";
|
|
2788
3211
|
const critiqueNotesLoaded = Boolean(critiqueNotes) && normalizedEditor === latestCritiqueNotesNormalized;
|
|
2789
3212
|
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
loadCritiqueFullBtn.hidden = true;
|
|
2794
|
-
|
|
2795
|
-
loadResponseBtn.disabled = uiBusy || !hasThinking || thinkingLoaded;
|
|
2796
|
-
loadResponseBtn.textContent = !hasThinking
|
|
2797
|
-
? "Thinking unavailable"
|
|
2798
|
-
: (thinkingLoaded ? "Thinking already in editor" : "Load thinking into editor");
|
|
3213
|
+
loadResponseBtn.hidden = isCritiqueResponse;
|
|
3214
|
+
loadCritiqueNotesBtn.hidden = !isCritiqueResponse;
|
|
3215
|
+
loadCritiqueFullBtn.hidden = !isCritiqueResponse;
|
|
2799
3216
|
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
} else {
|
|
2803
|
-
loadResponseBtn.hidden = isCritiqueResponse;
|
|
2804
|
-
loadCritiqueNotesBtn.hidden = !isCritiqueResponse;
|
|
2805
|
-
loadCritiqueFullBtn.hidden = !isCritiqueResponse;
|
|
2806
|
-
|
|
2807
|
-
loadResponseBtn.disabled = uiBusy || !hasResponse || responseLoaded || isCritiqueResponse;
|
|
2808
|
-
loadResponseBtn.textContent = responseLoaded ? "Response already in editor" : "Load response into editor";
|
|
3217
|
+
loadResponseBtn.disabled = uiBusy || !hasResponse || responseLoaded || isCritiqueResponse;
|
|
3218
|
+
loadResponseBtn.textContent = responseLoaded ? "Response already in editor" : "Load response into editor";
|
|
2809
3219
|
|
|
2810
|
-
|
|
2811
|
-
|
|
3220
|
+
loadCritiqueNotesBtn.disabled = uiBusy || !isCritiqueResponse || !critiqueNotes || critiqueNotesLoaded;
|
|
3221
|
+
loadCritiqueNotesBtn.textContent = critiqueNotesLoaded ? "Critique notes already in editor" : "Load critique notes into editor";
|
|
2812
3222
|
|
|
2813
|
-
|
|
2814
|
-
|
|
3223
|
+
loadCritiqueFullBtn.disabled = uiBusy || !isCritiqueResponse || responseLoaded;
|
|
3224
|
+
loadCritiqueFullBtn.textContent = responseLoaded ? "Full critique already in editor" : "Load full critique into editor";
|
|
2815
3225
|
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
}
|
|
3226
|
+
copyResponseBtn.disabled = uiBusy || !hasResponse;
|
|
3227
|
+
copyResponseBtn.textContent = "Copy response text";
|
|
2819
3228
|
|
|
2820
3229
|
const rightPaneShowsPreview = rightView === "preview" || rightView === "editor-preview";
|
|
2821
3230
|
const exportText = rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown;
|
|
2822
3231
|
const canExportPdf = rightPaneShowsPreview && Boolean(String(exportText || "").trim());
|
|
2823
3232
|
if (exportPdfBtn) {
|
|
2824
3233
|
exportPdfBtn.disabled = uiBusy || pdfExportInProgress || !canExportPdf;
|
|
2825
|
-
if (rightView === "
|
|
2826
|
-
exportPdfBtn.title = "
|
|
3234
|
+
if (rightView === "trace") {
|
|
3235
|
+
exportPdfBtn.title = "Working view does not support PDF export.";
|
|
2827
3236
|
} else if (rightView === "markdown") {
|
|
2828
3237
|
exportPdfBtn.title = "Switch right pane to Response (Preview) or Editor (Preview) to export PDF.";
|
|
2829
3238
|
} else if (!canExportPdf) {
|
|
@@ -3052,12 +3461,16 @@
|
|
|
3052
3461
|
}
|
|
3053
3462
|
|
|
3054
3463
|
function setRightView(nextView) {
|
|
3464
|
+
const previousView = rightView;
|
|
3055
3465
|
rightView = nextView === "preview"
|
|
3056
3466
|
? "preview"
|
|
3057
3467
|
: (nextView === "editor-preview"
|
|
3058
3468
|
? "editor-preview"
|
|
3059
|
-
: (nextView === "thinking" ? "
|
|
3469
|
+
: ((nextView === "trace" || nextView === "thinking") ? "trace" : "markdown"));
|
|
3060
3470
|
rightViewSelect.value = rightView;
|
|
3471
|
+
if (rightView === "trace" && previousView !== "trace") {
|
|
3472
|
+
traceAutoScroll = true;
|
|
3473
|
+
}
|
|
3061
3474
|
|
|
3062
3475
|
if (rightView !== "editor-preview" && responseEditorPreviewTimer) {
|
|
3063
3476
|
window.clearTimeout(responseEditorPreviewTimer);
|
|
@@ -8580,6 +8993,10 @@
|
|
|
8580
8993
|
}
|
|
8581
8994
|
}
|
|
8582
8995
|
|
|
8996
|
+
if (message.traceState) {
|
|
8997
|
+
replaceTraceState(message.traceState);
|
|
8998
|
+
}
|
|
8999
|
+
|
|
8583
9000
|
let appliedHistory = false;
|
|
8584
9001
|
if (Array.isArray(message.responseHistory)) {
|
|
8585
9002
|
appliedHistory = setResponseHistory(message.responseHistory, {
|
|
@@ -8626,6 +9043,26 @@
|
|
|
8626
9043
|
return;
|
|
8627
9044
|
}
|
|
8628
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
|
+
|
|
8629
9066
|
if (message.type === "request_started") {
|
|
8630
9067
|
pendingRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
|
|
8631
9068
|
pendingKind = typeof message.kind === "string" ? message.kind : "unknown";
|
|
@@ -9323,6 +9760,8 @@
|
|
|
9323
9760
|
setRightView(rightViewSelect.value);
|
|
9324
9761
|
});
|
|
9325
9762
|
|
|
9763
|
+
attachResponsePaneInteractionHandlers();
|
|
9764
|
+
|
|
9326
9765
|
followSelect.addEventListener("change", () => {
|
|
9327
9766
|
followLatest = followSelect.value !== "off";
|
|
9328
9767
|
if (followLatest && queuedLatestResponse) {
|
|
@@ -9565,17 +10004,6 @@
|
|
|
9565
10004
|
});
|
|
9566
10005
|
|
|
9567
10006
|
loadResponseBtn.addEventListener("click", () => {
|
|
9568
|
-
if (rightView === "thinking") {
|
|
9569
|
-
if (!latestResponseThinking.trim()) {
|
|
9570
|
-
setStatus("No thinking available for the selected response.", "warning");
|
|
9571
|
-
return;
|
|
9572
|
-
}
|
|
9573
|
-
setEditorText(latestResponseThinking, { preserveScroll: false, preserveSelection: false });
|
|
9574
|
-
setSourceState({ source: "blank", label: "assistant thinking", path: null });
|
|
9575
|
-
setStatus("Loaded thinking into editor.", "success");
|
|
9576
|
-
return;
|
|
9577
|
-
}
|
|
9578
|
-
|
|
9579
10007
|
if (!latestResponseMarkdown.trim()) {
|
|
9580
10008
|
setStatus("No response available yet.", "warning");
|
|
9581
10009
|
return;
|
|
@@ -9614,15 +10042,15 @@
|
|
|
9614
10042
|
});
|
|
9615
10043
|
|
|
9616
10044
|
copyResponseBtn.addEventListener("click", async () => {
|
|
9617
|
-
const content =
|
|
10045
|
+
const content = latestResponseMarkdown;
|
|
9618
10046
|
if (!content.trim()) {
|
|
9619
|
-
setStatus(
|
|
10047
|
+
setStatus("No response available yet.", "warning");
|
|
9620
10048
|
return;
|
|
9621
10049
|
}
|
|
9622
10050
|
|
|
9623
10051
|
try {
|
|
9624
10052
|
await navigator.clipboard.writeText(content);
|
|
9625
|
-
setStatus(
|
|
10053
|
+
setStatus("Copied response text.", "success");
|
|
9626
10054
|
} catch (error) {
|
|
9627
10055
|
setStatus("Clipboard write failed.", "warning");
|
|
9628
10056
|
}
|