pi-studio 0.9.42 → 0.9.44

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.
@@ -1,4 +1,4 @@
1
- (() => {
1
+ (async () => {
2
2
  const statusLineEl = document.getElementById("statusLine");
3
3
  const statusEl = document.getElementById("status");
4
4
  const statusSpinnerEl = document.getElementById("statusSpinner");
@@ -81,6 +81,8 @@
81
81
  const pullLatestBtn = document.getElementById("pullLatestBtn");
82
82
  const insertHeaderBtn = document.getElementById("insertHeaderBtn");
83
83
  const critiqueBtn = document.getElementById("critiqueBtn");
84
+ const showMeBtn = document.getElementById("showMeBtn");
85
+ const showMeResponseBtn = document.getElementById("showMeResponseBtn");
84
86
  const quizBtn = document.getElementById("quizBtn");
85
87
  const lensSelect = document.getElementById("lensSelect");
86
88
  const fileInput = document.getElementById("fileInput");
@@ -90,6 +92,7 @@
90
92
  const resourceDirInput = document.getElementById("resourceDirInput");
91
93
  const resourceDirClearBtn = document.getElementById("resourceDirClearBtn");
92
94
  const loadResponseBtn = document.getElementById("loadResponseBtn");
95
+ const annotateResponseBtn = document.getElementById("annotateResponseBtn");
93
96
  const loadCritiqueNotesBtn = document.getElementById("loadCritiqueNotesBtn");
94
97
  const loadCritiqueFullBtn = document.getElementById("loadCritiqueFullBtn");
95
98
  const copyResponseBtn = document.getElementById("copyResponseBtn");
@@ -209,9 +212,21 @@
209
212
  };
210
213
 
211
214
  const navigationHelpers = globalThis.PiStudioNavigationHelpers;
212
- if (!navigationHelpers || typeof navigationHelpers.readPaneFocusTarget !== "function") {
215
+ if (
216
+ !navigationHelpers
217
+ || typeof navigationHelpers.readPaneFocusTarget !== "function"
218
+ || typeof navigationHelpers.ensureStudioTabStateId !== "function"
219
+ || typeof navigationHelpers.readStudioWorkspaceState !== "function"
220
+ || typeof navigationHelpers.persistStudioWorkspaceState !== "function"
221
+ || typeof navigationHelpers.clearStudioWorkspaceState !== "function"
222
+ ) {
213
223
  throw new Error("Studio navigation helpers failed to load.");
214
224
  }
225
+ const showMeHelpers = globalThis.PiStudioShowMeHelpers;
226
+ if (!showMeHelpers || typeof showMeHelpers.chooseStudioShowMeFocus !== "function") {
227
+ throw new Error("Studio Show me helpers failed to load.");
228
+ }
229
+ const studioTabStateId = navigationHelpers.ensureStudioTabStateId(window);
215
230
  const initialQueryParams = new URLSearchParams(window.location.search || "");
216
231
  const initialPaneFocusTarget = navigationHelpers.readPaneFocusTarget(window.location);
217
232
  const skipInitialWorkspaceRestore = initialQueryParams.get("skipWorkspaceRestore") === "1";
@@ -2059,8 +2074,9 @@
2059
2074
  const PANE_SPLIT_MIN_PERCENT = 20;
2060
2075
  const PANE_SPLIT_MAX_PERCENT = 80;
2061
2076
  const PANE_SPLIT_SNAP_TO_CENTER_PERCENT = 1;
2062
- const STUDIO_WORKSPACE_STORAGE_KEY = "piStudio.workspaceState.v1";
2063
2077
  const STUDIO_WORKSPACE_MAX_TEXT_CHARS = 900_000;
2078
+ const STUDIO_WORKSPACE_PERSIST_INTERVAL_MS = 300;
2079
+ const STUDIO_WORKSPACE_RECOVERY_FETCH_TIMEOUT_MS = 1_500;
2064
2080
  const EDITOR_HIGHLIGHT_MAX_CHARS = 100_000;
2065
2081
  const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
2066
2082
  const EDITOR_LANGUAGE_STORAGE_KEY = "piStudio.editorLanguage";
@@ -2217,6 +2233,8 @@
2217
2233
  let workspacePersistenceReady = false;
2218
2234
  let workspacePersistTimer = null;
2219
2235
  let workspaceRestoredFromBrowser = false;
2236
+ let workspaceServerPersistenceBlocked = false;
2237
+ let lastWorkspacePersistenceSavedAt = 0;
2220
2238
  let suppressedEditorSelectionStart = null;
2221
2239
  let suppressedEditorSelectionEnd = null;
2222
2240
  const previewJumpHighlightState = new WeakMap();
@@ -2528,6 +2546,7 @@
2528
2546
  buttonEl.setAttribute("aria-expanded", "false");
2529
2547
  buttonEl.addEventListener("click", (event) => {
2530
2548
  event.stopPropagation();
2549
+ if (name === "review") syncShowMeButton();
2531
2550
  if (name === "review" && getAbortablePendingKind() === "critique") {
2532
2551
  requestCancelForPendingRequest("critique");
2533
2552
  return;
@@ -2549,7 +2568,7 @@
2549
2568
  if (!isEditorOnlyMode && critiqueBtn && lensSelect) {
2550
2569
  const reviewButton = makeStudioUiRefreshElement("button", "studio-refresh-tool-tab studio-refresh-review-btn", "Review");
2551
2570
  reviewMenu = makeStudioUiRefreshMenu(reviewButton, "review", "studio-refresh-review-anchor");
2552
- appendStudioUiRefreshMenuSection(reviewMenu.menu, "Action", [critiqueBtn, quizBtn]);
2571
+ appendStudioUiRefreshMenuSection(reviewMenu.menu, "Action", [critiqueBtn, showMeBtn, showMeResponseBtn, quizBtn]);
2553
2572
  appendStudioUiRefreshMenuSection(reviewMenu.menu, "Setting", [lensSelect]);
2554
2573
  }
2555
2574
 
@@ -2875,7 +2894,7 @@
2875
2894
  if (isEditorOnlyMode) {
2876
2895
  return "Editor-only mode: edit, browse files, annotate, preview, save, suggest, refresh file-backed text, or send to a REPL.";
2877
2896
  }
2878
- return "Edit, load, or annotate text, then run, save, send to pi editor, or critique.";
2897
+ return "Edit, load, or annotate text, then run, save, review, or ask Studio to show the current structure.";
2879
2898
  }
2880
2899
 
2881
2900
  function normalizeTerminalPhase(phase) {
@@ -2943,6 +2962,7 @@
2943
2962
  function getStudioActionLabel(kind) {
2944
2963
  if (kind === "annotation") return "sending annotated reply";
2945
2964
  if (kind === "critique") return "running critique";
2965
+ if (kind === "show-me") return "building a compact explanation";
2946
2966
  if (kind === "direct") return "running editor text";
2947
2967
  if (kind === "compact") return "compacting context";
2948
2968
  if (kind === "send_to_editor") return "sending to pi editor";
@@ -3128,7 +3148,7 @@
3128
3148
  }
3129
3149
 
3130
3150
  function isTitleAttentionRequestKind(kind) {
3131
- return kind === "annotation" || kind === "critique" || kind === "direct";
3151
+ return kind === "annotation" || kind === "critique" || kind === "show-me" || kind === "direct";
3132
3152
  }
3133
3153
 
3134
3154
  function armTitleAttentionForRequest(requestId, kind) {
@@ -3162,6 +3182,7 @@
3162
3182
 
3163
3183
  function getTitleAttentionMessage(kind) {
3164
3184
  if (kind === "critique") return "● Critique ready";
3185
+ if (kind === "show-me") return "● Visual explanation ready";
3165
3186
  if (kind === "direct") return "● Response ready";
3166
3187
  return "● Reply ready";
3167
3188
  }
@@ -3199,6 +3220,7 @@
3199
3220
  function getTitleActionMessage(kind) {
3200
3221
  if (kind === "annotation") return "Replying…";
3201
3222
  if (kind === "critique") return "Critiquing…";
3223
+ if (kind === "show-me") return "Showing…";
3202
3224
  if (kind === "direct") return "Running…";
3203
3225
  if (kind === "compact") return "Compacting…";
3204
3226
  if (kind === "send_to_editor") return "Sending to editor…";
@@ -3239,6 +3261,7 @@
3239
3261
 
3240
3262
  if (terminalActivityPhase === "responding") {
3241
3263
  if (activeKind === "critique") return "Critiquing…";
3264
+ if (activeKind === "show-me") return "Showing…";
3242
3265
  if (activeKind === "annotation") return "Replying…";
3243
3266
  if (activeKind === "direct") return "Thinking…";
3244
3267
  return "Working…";
@@ -3605,11 +3628,13 @@
3605
3628
  });
3606
3629
 
3607
3630
  document.addEventListener("visibilitychange", () => {
3608
- if (!document.hidden) {
3609
- windowHasFocus = typeof document.hasFocus === "function" ? document.hasFocus() : windowHasFocus;
3610
- if (windowHasFocus) {
3611
- clearTitleAttention();
3612
- }
3631
+ if (document.hidden) {
3632
+ flushWorkspacePersistence({ beacon: false });
3633
+ return;
3634
+ }
3635
+ windowHasFocus = typeof document.hasFocus === "function" ? document.hasFocus() : windowHasFocus;
3636
+ if (windowHasFocus) {
3637
+ clearTitleAttention();
3613
3638
  }
3614
3639
  });
3615
3640
 
@@ -3863,6 +3888,7 @@
3863
3888
  document.body.classList.add("pane-focus-right");
3864
3889
  }
3865
3890
  updatePaneFocusButtons();
3891
+ updateResultActionButtons();
3866
3892
  }
3867
3893
 
3868
3894
  function persistPaneFocusUrlState() {
@@ -4416,7 +4442,7 @@
4416
4442
 
4417
4443
  if (plainEscape) {
4418
4444
  const activeKind = getAbortablePendingKind();
4419
- if (activeKind === "direct" || activeKind === "critique") {
4445
+ if (activeKind === "direct" || activeKind === "critique" || activeKind === "show-me") {
4420
4446
  event.preventDefault();
4421
4447
  requestCancelForPendingRequest(activeKind);
4422
4448
  return;
@@ -4494,7 +4520,10 @@
4494
4520
  }
4495
4521
 
4496
4522
  function normalizeHistoryKind(kind) {
4497
- return kind === "critique" ? "critique" : "annotation";
4523
+ if (kind === "critique") return "critique";
4524
+ if (kind === "show-me") return "show-me";
4525
+ if (kind === "direct") return "direct";
4526
+ return "annotation";
4498
4527
  }
4499
4528
 
4500
4529
  function normalizeTraceSummary(summary) {
@@ -4564,6 +4593,61 @@
4564
4593
  return responseHistory[responseHistoryIndex] || null;
4565
4594
  }
4566
4595
 
4596
+ function getStudioShowMeFocus(target) {
4597
+ const selection = getEditorSelectionRange();
4598
+ const selectedResponse = getSelectedHistoryItem();
4599
+ const responseText = selectedResponse && typeof selectedResponse.markdown === "string"
4600
+ ? selectedResponse.markdown
4601
+ : latestResponseMarkdown;
4602
+ const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
4603
+ const selected = total > 0 && responseHistoryIndex >= 0 && responseHistoryIndex < total
4604
+ ? responseHistoryIndex + 1
4605
+ : 0;
4606
+ return showMeHelpers.chooseStudioShowMeFocus({
4607
+ target: target === "response" ? "response" : "editor",
4608
+ selectionText: selection.selected,
4609
+ responseText,
4610
+ responseVisible: (rightView === "markdown" || rightView === "preview") && paneFocusTarget !== "left",
4611
+ editorText: sourceTextEl.value,
4612
+ responseIndex: selected,
4613
+ responseTotal: total,
4614
+ });
4615
+ }
4616
+
4617
+ function syncShowMeButton() {
4618
+ if (!showMeBtn) return;
4619
+ const showMeIsStop = getAbortablePendingKind() === "show-me";
4620
+ const unavailableForRunChain = studioRunChainActive && !showMeIsStop;
4621
+ const editorFocus = getStudioShowMeFocus("editor");
4622
+ const responseFocus = getStudioShowMeFocus("response");
4623
+ const showEditorAction = showMeIsStop || editorFocus.sourceKind !== "context" || !responseFocus;
4624
+ showMeBtn.hidden = !showEditorAction;
4625
+ const editorMenuItem = showMeBtn.closest(".studio-refresh-menu-item");
4626
+ if (editorMenuItem) editorMenuItem.hidden = !showEditorAction;
4627
+ showMeBtn.textContent = showMeIsStop ? "Stop showing" : editorFocus.actionLabel;
4628
+ showMeBtn.classList.toggle("request-stop-active", showMeIsStop);
4629
+ showMeBtn.disabled = showMeIsStop
4630
+ ? wsState === "Disconnected"
4631
+ : isEditorOnlyMode || wsState === "Disconnected" || uiBusy || unavailableForRunChain;
4632
+ showMeBtn.title = showMeIsStop
4633
+ ? "Stop the running Show me request. Shortcut: Esc."
4634
+ : (isEditorOnlyMode
4635
+ ? "Show me is unavailable in editor-only mode."
4636
+ : (unavailableForRunChain
4637
+ ? "Show me is unavailable while Run editor text is active."
4638
+ : "Explain the " + editorFocus.sourceLabel + " using the smallest useful visual or structural representation."));
4639
+ if (showMeResponseBtn) {
4640
+ showMeResponseBtn.hidden = !responseFocus;
4641
+ const responseMenuItem = showMeResponseBtn.closest(".studio-refresh-menu-item");
4642
+ if (responseMenuItem) responseMenuItem.hidden = !responseFocus;
4643
+ showMeResponseBtn.disabled = !responseFocus || showMeIsStop || isEditorOnlyMode || wsState === "Disconnected" || uiBusy || unavailableForRunChain;
4644
+ showMeResponseBtn.textContent = "Explain displayed response";
4645
+ showMeResponseBtn.title = responseFocus
4646
+ ? "Explain the " + responseFocus.sourceLabel + " using the smallest useful visual or structural representation."
4647
+ : "Available when a response is displayed in the right pane.";
4648
+ }
4649
+ }
4650
+
4567
4651
  function syncTraceForSelectedHistoryItem() {
4568
4652
  const item = getSelectedHistoryItem();
4569
4653
  const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
@@ -4651,6 +4735,7 @@
4651
4735
  : "Load the prompt that generated the selected response into the editor.")
4652
4736
  : "Prompt unavailable for the selected response.";
4653
4737
  }
4738
+ syncShowMeButton();
4654
4739
  }
4655
4740
 
4656
4741
  function applySelectedHistoryItem(options) {
@@ -4685,7 +4770,9 @@
4685
4770
  if (applied && !(options && options.silent)) {
4686
4771
  const item = getSelectedHistoryItem();
4687
4772
  if (item) {
4688
- const responseLabel = item.kind === "critique" ? "critique" : "response";
4773
+ const responseLabel = item.kind === "critique"
4774
+ ? "critique"
4775
+ : (item.kind === "show-me" ? "visual explanation" : "response");
4689
4776
  setStatus("Viewing " + responseLabel + " in current branch history " + (nextIndex + 1) + "/" + total + ".");
4690
4777
  }
4691
4778
  }
@@ -4827,7 +4914,9 @@
4827
4914
  }
4828
4915
 
4829
4916
  const time = formatReferenceTime(latestResponseTimestamp);
4830
- const responseLabel = latestResponseKind === "critique" ? "assistant critique" : "assistant response";
4917
+ const responseLabel = latestResponseKind === "critique"
4918
+ ? "assistant critique"
4919
+ : (latestResponseKind === "show-me" ? "visual explanation" : "assistant response");
4831
4920
  const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
4832
4921
  const selected = total > 0 && responseHistoryIndex >= 0 && responseHistoryIndex < total
4833
4922
  ? responseHistoryIndex + 1
@@ -10971,12 +11060,23 @@
10971
11060
  const critiqueNotesLoaded = Boolean(critiqueNotes) && normalizedEditor === latestCritiqueNotesNormalized;
10972
11061
 
10973
11062
  loadResponseBtn.hidden = isCritiqueResponse;
11063
+ annotateResponseBtn.hidden = isCritiqueResponse;
10974
11064
  loadCritiqueNotesBtn.hidden = !isCritiqueResponse;
10975
11065
  loadCritiqueFullBtn.hidden = !isCritiqueResponse;
10976
11066
 
10977
11067
  loadResponseBtn.disabled = uiBusy || !hasResponse || responseLoaded || isCritiqueResponse;
10978
11068
  loadResponseBtn.textContent = responseLoaded ? "Response already in editor" : "Load response into editor";
10979
11069
 
11070
+ const annotationWorkspaceReady = responseLoaded
11071
+ && editorView === "markdown"
11072
+ && rightView === "editor-preview"
11073
+ && paneFocusTarget === "off";
11074
+ annotateResponseBtn.disabled = uiBusy || !hasResponse || isCritiqueResponse || annotationWorkspaceReady;
11075
+ annotateResponseBtn.textContent = annotationWorkspaceReady ? "Response ready to annotate" : "Annotate response";
11076
+ annotateResponseBtn.title = annotationWorkspaceReady
11077
+ ? "The selected response is exactly in the raw editor with Editor Preview open."
11078
+ : "Load the selected response into the raw editor and show Editor Preview. This replaces the current editor text.";
11079
+
10980
11080
  loadCritiqueNotesBtn.disabled = uiBusy || !isCritiqueResponse || !critiqueNotes || critiqueNotesLoaded;
10981
11081
  loadCritiqueNotesBtn.textContent = critiqueNotesLoaded ? "Critique notes already in editor" : "Load critique notes into editor";
10982
11082
 
@@ -11061,6 +11161,7 @@
11061
11161
 
11062
11162
  updateSyncBadge(normalizedEditor);
11063
11163
  syncStudioQuartoDirtyUi();
11164
+ syncShowMeButton();
11064
11165
  }
11065
11166
 
11066
11167
  function refreshResponseUi() {
@@ -11377,34 +11478,46 @@
11377
11478
  return "source:" + normalized.source + ":" + normalized.label;
11378
11479
  }
11379
11480
 
11380
- function getWorkspacePersistenceStorage() {
11481
+ function readPersistedWorkspaceState() {
11381
11482
  try {
11382
- return window.sessionStorage || null;
11483
+ return navigationHelpers.readStudioWorkspaceState(window, studioTabStateId);
11383
11484
  } catch {
11384
11485
  return null;
11385
11486
  }
11386
11487
  }
11387
11488
 
11388
- function clearLegacyWorkspacePersistenceStorage() {
11389
- try {
11390
- if (window.localStorage) window.localStorage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
11391
- } catch {}
11392
- }
11393
-
11394
- function readPersistedWorkspaceState() {
11489
+ async function readServerWorkspaceRecoveryState() {
11490
+ if (skipInitialWorkspaceRestore) return { status: "skipped", state: null };
11491
+ const controller = typeof AbortController === "function" ? new AbortController() : null;
11492
+ const timer = controller
11493
+ ? window.setTimeout(() => controller.abort(), STUDIO_WORKSPACE_RECOVERY_FETCH_TIMEOUT_MS)
11494
+ : null;
11395
11495
  try {
11396
- const storage = getWorkspacePersistenceStorage();
11397
- const raw = storage ? storage.getItem(STUDIO_WORKSPACE_STORAGE_KEY) : null;
11398
- if (!raw) return null;
11399
- const parsed = JSON.parse(raw);
11400
- if (!parsed || typeof parsed !== "object" || parsed.version !== 1) return null;
11401
- if (typeof parsed.text !== "string") return null;
11402
- return parsed;
11496
+ const payload = await fetchStudioJson("/tab-workspace-state", {
11497
+ query: { tabStateId: studioTabStateId },
11498
+ signal: controller ? controller.signal : undefined,
11499
+ });
11500
+ const state = payload && payload.state && typeof payload.state === "object" ? payload.state : null;
11501
+ return { status: state ? "found" : "empty", state };
11403
11502
  } catch {
11404
- return null;
11503
+ return { status: "unavailable", state: null };
11504
+ } finally {
11505
+ if (timer !== null) window.clearTimeout(timer);
11405
11506
  }
11406
11507
  }
11407
11508
 
11509
+ function getWorkspaceStateSavedAt(state) {
11510
+ return state && typeof state.savedAt === "number" && Number.isFinite(state.savedAt)
11511
+ ? state.savedAt
11512
+ : 0;
11513
+ }
11514
+
11515
+ function chooseNewestRestorableWorkspaceState(states) {
11516
+ return states
11517
+ .filter((state) => shouldRestorePersistedWorkspaceState(state))
11518
+ .sort((a, b) => getWorkspaceStateSavedAt(b) - getWorkspaceStateSavedAt(a))[0] || null;
11519
+ }
11520
+
11408
11521
  function shouldRestorePersistedWorkspaceState(state) {
11409
11522
  if (skipInitialWorkspaceRestore) return false;
11410
11523
  if (!state || typeof state.text !== "string") return false;
@@ -11417,9 +11530,10 @@
11417
11530
  }
11418
11531
 
11419
11532
  function buildWorkspacePersistencePayload() {
11533
+ lastWorkspacePersistenceSavedAt = Math.max(Date.now(), lastWorkspacePersistenceSavedAt + 1);
11420
11534
  return {
11421
11535
  version: 1,
11422
- savedAt: Date.now(),
11536
+ savedAt: lastWorkspacePersistenceSavedAt,
11423
11537
  sourceState: normalizeWorkspaceSourceState(sourceState),
11424
11538
  resourceDir: getCurrentResourceDirValue(),
11425
11539
  editorView,
@@ -11434,38 +11548,63 @@
11434
11548
  };
11435
11549
  }
11436
11550
 
11437
- function persistWorkspaceStateNow() {
11551
+ function sendServerWorkspaceRecoveryState(payload, options) {
11552
+ if ((options && options.skipServer) || (workspaceServerPersistenceBlocked && !(options && options.forceServer))) return;
11553
+ const body = { tabStateId: studioTabStateId, state: payload };
11554
+ if (ws && ws.readyState === WebSocket.OPEN) {
11555
+ try {
11556
+ ws.send(JSON.stringify({ type: "workspace_state_update", ...body }));
11557
+ return;
11558
+ } catch {
11559
+ // Fall through to the authenticated HTTP path.
11560
+ }
11561
+ }
11562
+ if (options && options.beacon && trySendStudioJsonBeacon("/tab-workspace-state", body)) return;
11563
+ void fetchStudioJson("/tab-workspace-state", {
11564
+ method: "POST",
11565
+ body: JSON.stringify(body),
11566
+ keepalive: Boolean(options && options.beacon),
11567
+ }).catch(() => {
11568
+ // Session storage remains the in-page fallback if server recovery fails.
11569
+ });
11570
+ }
11571
+
11572
+ function persistWorkspaceStateNow(options) {
11438
11573
  if (!workspacePersistenceReady) return;
11439
11574
  try {
11440
- const storage = getWorkspacePersistenceStorage();
11441
- if (!storage) return;
11442
- clearLegacyWorkspacePersistenceStorage();
11443
11575
  const payload = buildWorkspacePersistencePayload();
11444
11576
  if (payload.text.length > STUDIO_WORKSPACE_MAX_TEXT_CHARS) {
11445
- storage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
11577
+ navigationHelpers.clearStudioWorkspaceState(window, studioTabStateId);
11578
+ sendServerWorkspaceRecoveryState({
11579
+ ...payload,
11580
+ sourceState: { source: "recovery-omitted", label: "oversized editor", path: null, draftId: null },
11581
+ resourceDir: "",
11582
+ rightView: "editor-preview",
11583
+ text: "",
11584
+ }, { ...(options || {}), forceServer: true });
11446
11585
  return;
11447
11586
  }
11448
- storage.setItem(STUDIO_WORKSPACE_STORAGE_KEY, JSON.stringify(payload));
11587
+ navigationHelpers.persistStudioWorkspaceState(window, studioTabStateId, payload);
11588
+ sendServerWorkspaceRecoveryState(payload, options);
11449
11589
  } catch {
11450
- // Ignore browser storage failures and quota limits.
11590
+ // Ignore browser storage, serialization, and recovery-request failures.
11451
11591
  }
11452
11592
  }
11453
11593
 
11454
11594
  function scheduleWorkspacePersistence() {
11455
- if (!workspacePersistenceReady) return;
11456
- if (workspacePersistTimer !== null) window.clearTimeout(workspacePersistTimer);
11595
+ if (!workspacePersistenceReady || workspacePersistTimer !== null) return;
11457
11596
  workspacePersistTimer = window.setTimeout(() => {
11458
11597
  workspacePersistTimer = null;
11459
11598
  persistWorkspaceStateNow();
11460
- }, 160);
11599
+ }, STUDIO_WORKSPACE_PERSIST_INTERVAL_MS);
11461
11600
  }
11462
11601
 
11463
- function flushWorkspacePersistence() {
11602
+ function flushWorkspacePersistence(options) {
11464
11603
  if (workspacePersistTimer !== null) {
11465
11604
  window.clearTimeout(workspacePersistTimer);
11466
11605
  workspacePersistTimer = null;
11467
11606
  }
11468
- persistWorkspaceStateNow();
11607
+ persistWorkspaceStateNow(options || { beacon: true });
11469
11608
  }
11470
11609
 
11471
11610
  function clearPersistedWorkspaceState() {
@@ -11474,10 +11613,16 @@
11474
11613
  workspacePersistTimer = null;
11475
11614
  }
11476
11615
  try {
11477
- const storage = getWorkspacePersistenceStorage();
11478
- if (storage) storage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
11616
+ navigationHelpers.clearStudioWorkspaceState(window, studioTabStateId);
11617
+ const marker = buildWorkspacePersistencePayload();
11618
+ sendServerWorkspaceRecoveryState({
11619
+ ...marker,
11620
+ sourceState: { source: "recovery-cleared", label: "cleared editor", path: null, draftId: null },
11621
+ resourceDir: "",
11622
+ rightView: "editor-preview",
11623
+ text: "",
11624
+ }, { beacon: true, forceServer: true });
11479
11625
  } catch {}
11480
- clearLegacyWorkspacePersistenceStorage();
11481
11626
  }
11482
11627
 
11483
11628
  function applyPersistedWorkspaceState(state) {
@@ -12555,6 +12700,8 @@
12555
12700
  headers,
12556
12701
  body: init.body,
12557
12702
  cache: "no-store",
12703
+ signal: init.signal,
12704
+ keepalive: init.keepalive === true,
12558
12705
  });
12559
12706
  let payload = null;
12560
12707
  try {
@@ -19202,7 +19349,7 @@
19202
19349
 
19203
19350
  function getAbortablePendingKind() {
19204
19351
  if (!pendingRequestId) return null;
19205
- return pendingKind === "direct" || pendingKind === "critique" ? pendingKind : null;
19352
+ return pendingKind === "direct" || pendingKind === "critique" || pendingKind === "show-me" ? pendingKind : null;
19206
19353
  }
19207
19354
 
19208
19355
  function requestCancelForPendingRequest(expectedKind) {
@@ -19345,6 +19492,7 @@
19345
19492
  ? "Resume the current Studio quiz."
19346
19493
  : "Open an active quiz for the current editor selection or document.");
19347
19494
  }
19495
+ syncShowMeButton();
19348
19496
  syncStudioUiRefreshReviewTrigger();
19349
19497
  }
19350
19498
 
@@ -19427,7 +19575,7 @@
19427
19575
  ? timestamp
19428
19576
  : Date.now();
19429
19577
  const responseThinking = typeof thinking === "string" ? thinking : "";
19430
- const responseKind = kind === "critique" ? "critique" : "annotation";
19578
+ const responseKind = normalizeHistoryKind(kind);
19431
19579
  const resetScroll = options && Object.prototype.hasOwnProperty.call(options, "resetScroll")
19432
19580
  ? Boolean(options.resetScroll)
19433
19581
  : (
@@ -19445,7 +19593,7 @@
19445
19593
  latestResponseThinking = responseThinking;
19446
19594
  latestResponseKind = responseKind;
19447
19595
  latestResponseTimestamp = responseTimestamp;
19448
- latestResponseIsStructuredCritique = isStructuredCritique(markdown);
19596
+ latestResponseIsStructuredCritique = responseKind === "critique" && isStructuredCritique(markdown);
19449
19597
  latestResponseHasContent = Boolean(markdown && markdown.trim());
19450
19598
  latestResponseNormalized = normalizeForCompare(markdown);
19451
19599
  latestResponseThinkingNormalized = normalizeForCompare(latestResponseThinking);
@@ -19463,7 +19611,7 @@
19463
19611
 
19464
19612
  function applyLatestPayload(payload, options) {
19465
19613
  if (!payload || typeof payload.markdown !== "string") return false;
19466
- const responseKind = payload.kind === "critique" ? "critique" : "annotation";
19614
+ const responseKind = normalizeHistoryKind(payload.kind);
19467
19615
  handleIncomingResponse(payload.markdown, responseKind, payload.timestamp, payload.thinking, options);
19468
19616
  return true;
19469
19617
  }
@@ -19674,8 +19822,8 @@
19674
19822
  if (!appliedHistory && message.lastResponse && typeof message.lastResponse.markdown === "string") {
19675
19823
  const lastMarkdown = message.lastResponse.markdown;
19676
19824
  const lastResponseKind =
19677
- message.lastResponse.kind === "critique"
19678
- ? "critique"
19825
+ typeof message.lastResponse.kind === "string"
19826
+ ? normalizeHistoryKind(message.lastResponse.kind)
19679
19827
  : (isStructuredCritique(lastMarkdown) ? "critique" : "annotation");
19680
19828
  handleIncomingResponse(lastMarkdown, lastResponseKind, message.lastResponse.timestamp, message.lastResponse.thinking);
19681
19829
  }
@@ -19919,7 +20067,7 @@
19919
20067
  const responseKind =
19920
20068
  typeof message.kind === "string"
19921
20069
  ? message.kind
19922
- : (pendingKind === "critique" ? "critique" : "annotation");
20070
+ : normalizeHistoryKind(pendingKind);
19923
20071
 
19924
20072
  stickyStudioKind = responseKind;
19925
20073
  pendingRequestId = null;
@@ -19944,6 +20092,8 @@
19944
20092
 
19945
20093
  if (responseKind === "critique") {
19946
20094
  setStatus("Critique ready.", "success");
20095
+ } else if (responseKind === "show-me") {
20096
+ setStatus("Visual explanation ready.", "success");
19947
20097
  } else if (responseKind === "direct") {
19948
20098
  setStatus("Model response ready.", "success");
19949
20099
  } else {
@@ -19970,7 +20120,7 @@
19970
20120
 
19971
20121
  if (typeof message.markdown === "string") {
19972
20122
  const payload = {
19973
- kind: message.kind === "critique" ? "critique" : "annotation",
20123
+ kind: normalizeHistoryKind(message.kind),
19974
20124
  markdown: message.markdown,
19975
20125
  thinking: typeof message.thinking === "string" ? message.thinking : null,
19976
20126
  timestamp: message.timestamp,
@@ -20909,6 +21059,7 @@
20909
21059
  }
20910
21060
  }
20911
21061
  updateEditorSelectionCommentUi();
21062
+ syncShowMeButton();
20912
21063
  });
20913
21064
 
20914
21065
  sourceTextEl.addEventListener("keyup", () => {
@@ -20987,20 +21138,94 @@
20987
21138
  }
20988
21139
  });
20989
21140
 
21141
+ function submitStudioShowMe(target) {
21142
+ const focus = getStudioShowMeFocus(target);
21143
+ if (!focus) {
21144
+ setStatus("That explanation source is no longer displayed.", "warning");
21145
+ syncShowMeButton();
21146
+ return;
21147
+ }
21148
+ const requestId = beginUiAction("show-me");
21149
+ if (!requestId) return;
21150
+ closeStudioUiRefreshMenus();
21151
+
21152
+ const sent = sendMessage({
21153
+ type: "show_me_request",
21154
+ requestId,
21155
+ sourceKind: focus.sourceKind,
21156
+ sourceLabel: focus.sourceLabel,
21157
+ sourceText: focus.sourceText,
21158
+ });
21159
+
21160
+ if (!sent) {
21161
+ pendingRequestId = null;
21162
+ pendingKind = null;
21163
+ setBusy(false);
21164
+ }
21165
+ }
21166
+
21167
+ if (showMeBtn) {
21168
+ showMeBtn.addEventListener("click", () => {
21169
+ if (getAbortablePendingKind() === "show-me") {
21170
+ requestCancelForPendingRequest("show-me");
21171
+ return;
21172
+ }
21173
+ submitStudioShowMe("editor");
21174
+ });
21175
+ }
21176
+
21177
+ if (showMeResponseBtn) {
21178
+ showMeResponseBtn.addEventListener("click", () => {
21179
+ if (getAbortablePendingKind() === "show-me") return;
21180
+ submitStudioShowMe("response");
21181
+ });
21182
+ }
21183
+
20990
21184
  if (quizBtn) {
20991
21185
  quizBtn.addEventListener("click", () => {
20992
21186
  openQuizOverlay();
20993
21187
  });
20994
21188
  }
20995
21189
 
20996
- loadResponseBtn.addEventListener("click", () => {
21190
+ function loadSelectedResponseIntoEditor(options) {
20997
21191
  if (!latestResponseMarkdown.trim()) {
20998
21192
  setStatus("No response available yet.", "warning");
20999
- return;
21193
+ return false;
21194
+ }
21195
+ const prepareForAnnotation = Boolean(options && options.annotate);
21196
+ const currentEditorText = String(sourceTextEl.value || "");
21197
+ const replacingEditedResponse = prepareForAnnotation
21198
+ && sourceState.source === "last-response"
21199
+ && Boolean(currentEditorText.trim())
21200
+ && normalizeForCompare(currentEditorText) !== latestResponseNormalized;
21201
+ if (
21202
+ replacingEditedResponse
21203
+ && !window.confirm("Replace your edited response with a fresh copy? Existing edits and annotations will be lost.")
21204
+ ) {
21205
+ setStatus("Kept the current editor text.");
21206
+ return false;
21000
21207
  }
21001
21208
  setEditorText(latestResponseMarkdown, { preserveScroll: false, preserveSelection: false });
21002
21209
  setSourceState({ source: "last-response", label: "last model response", path: null });
21210
+ if (prepareForAnnotation) {
21211
+ if (editorView !== "markdown") setEditorView("markdown");
21212
+ exitPaneFocus();
21213
+ setRightView("editor-preview");
21214
+ setActivePane("left");
21215
+ window.setTimeout(focusSourceTextNoScroll, 0);
21216
+ setStatus("Response loaded and Editor Preview opened for annotation.", "success");
21217
+ return true;
21218
+ }
21003
21219
  setStatus("Loaded response into editor.", "success");
21220
+ return true;
21221
+ }
21222
+
21223
+ loadResponseBtn.addEventListener("click", () => {
21224
+ loadSelectedResponseIntoEditor();
21225
+ });
21226
+
21227
+ annotateResponseBtn.addEventListener("click", () => {
21228
+ loadSelectedResponseIntoEditor({ annotate: true });
21004
21229
  });
21005
21230
 
21006
21231
  loadCritiqueNotesBtn.addEventListener("click", () => {
@@ -21892,13 +22117,26 @@
21892
22117
  setAnnotationsEnabled(initialAnnotationsEnabled, { silent: true });
21893
22118
  setReplSendMode(replSendMode);
21894
22119
 
21895
- const persistedWorkspaceState = readPersistedWorkspaceState();
22120
+ const sessionWorkspaceState = readPersistedWorkspaceState();
22121
+ const serverWorkspaceRecovery = await readServerWorkspaceRecoveryState();
22122
+ workspaceServerPersistenceBlocked = serverWorkspaceRecovery.status === "unavailable";
22123
+ const serverWorkspaceState = serverWorkspaceRecovery.state;
22124
+ lastWorkspacePersistenceSavedAt = Math.max(
22125
+ lastWorkspacePersistenceSavedAt,
22126
+ getWorkspaceStateSavedAt(sessionWorkspaceState),
22127
+ getWorkspaceStateSavedAt(serverWorkspaceState),
22128
+ );
22129
+ const persistedWorkspaceState = chooseNewestRestorableWorkspaceState([
22130
+ sessionWorkspaceState,
22131
+ serverWorkspaceState,
22132
+ ]);
21896
22133
  applyPersistedWorkspaceState(persistedWorkspaceState);
21897
22134
 
21898
22135
  setEditorView(editorView);
21899
22136
  setRightView(rightView);
21900
22137
  renderSourcePreview();
21901
22138
  workspacePersistenceReady = true;
22139
+ persistWorkspaceStateNow({ skipServer: serverWorkspaceRecovery.status === "unavailable" });
21902
22140
  if (workspaceRestoredFromBrowser) {
21903
22141
  setStatus("Restored editor workspace from this browser tab. Use Reset editor to discard it.", "success");
21904
22142
  }