pi-studio 0.9.21 → 0.9.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  All notable changes to `pi-studio` are documented here.
4
4
 
5
+ ## [0.9.23] — 2026-05-31
6
+
7
+ ### Changed
8
+ - Studio now treats Pi `/tree` navigation as an explicit response-history sync: it preserves the selected response only when that response still exists on the new branch, otherwise selects the latest response on the current branch and shows a status note that editor text was left unchanged.
9
+
10
+ ## [0.9.22] — 2026-05-29
11
+
12
+ ### Added
13
+ - Added **Open current file in new editor tab** and **Open current text as copy in new editor tab** to Source & context for explicit file-backed vs detached-copy tab opening.
14
+ - Added `Cmd/Ctrl+Alt+P` to switch the right pane directly back to Preview without cycling through Files, REPL, or Working.
15
+
16
+ ### Changed
17
+ - Changed the editor toolbar's detached-copy action into **New editor tab** for opening a blank editor tab, moved **Send current text to Pi editor** into Source & context, separated the passive origin summary from explicit **Detach from file** / **Reset origin** actions, and aligned local link menus with Files-view wording (**Open file tab** / **Convert tab**).
18
+
19
+ ### Fixed
20
+ - New Studio tabs opened from Files, local preview links, preview exports, or editor-tab actions now skip cloned browser-tab workspace restore on first load, preventing inherited Files/preview state from briefly flashing and replacing the requested document preview.
21
+
5
22
  ## [0.9.21] — 2026-05-28
6
23
 
7
24
  ### Added
@@ -177,6 +177,7 @@
177
177
  const isSshStudioSession = Boolean(document.body && document.body.dataset && document.body.dataset.sshSession === "1");
178
178
 
179
179
  const initialQueryParams = new URLSearchParams(window.location.search || "");
180
+ const skipInitialWorkspaceRestore = initialQueryParams.get("skipWorkspaceRestore") === "1";
180
181
  const explicitDocumentIdentityFromUrl = initialQueryParams.has("docId")
181
182
  || initialQueryParams.has("docSource")
182
183
  || initialQueryParams.has("docLabel")
@@ -232,6 +233,10 @@
232
233
  let pendingKind = null;
233
234
  let stickyStudioKind = null;
234
235
  const pendingCompanionWindows = new Map();
236
+ let sourceOriginSummaryEl = null;
237
+ let sourceResetOriginBtn = null;
238
+ let sourceOpenCurrentFileTabBtn = null;
239
+ let sourceOpenCurrentTextCopyTabBtn = null;
235
240
  let initialDocumentApplied = false;
236
241
  function normalizeRightViewValue(nextView) {
237
242
  const raw = String(nextView || "").trim();
@@ -258,8 +263,8 @@
258
263
  option.disabled = isEditorOnlyMode && !editorOnlyAllowed.has(option.value);
259
264
  });
260
265
  rightViewSelect.title = isEditorOnlyMode
261
- ? "Editor-only views: editor preview, Files, or REPL. Shortcut: F7 when the right pane is active; F6 switches panes."
262
- : "Right pane view mode. Shortcut: F7 when the right pane is active; F6 switches panes.";
266
+ ? "Editor-only views: editor preview, Files, or REPL. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+P switches directly to Preview."
267
+ : "Right pane view mode. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+P switches directly to Preview.";
263
268
  }
264
269
 
265
270
  function getInitialRightView(source) {
@@ -2405,8 +2410,66 @@
2405
2410
  suggestCompletionOptionsBtn.hidden = false;
2406
2411
  if (completionContextSelect) completionContextSelect.hidden = true;
2407
2412
  contextMenu = makeStudioUiRefreshMenu(suggestCompletionOptionsBtn, "context", "studio-refresh-context-anchor");
2408
- if (sourceBadgeEl) appendStudioUiRefreshMenuSection(contextMenu.menu, "Document", [sourceBadgeEl]);
2413
+ sourceOriginSummaryEl = makeStudioUiRefreshElement("div", "source-badge source-origin-summary", "Origin: blank");
2414
+ sourceOriginSummaryEl.setAttribute("aria-label", "Current editor origin");
2415
+ sourceResetOriginBtn = makeStudioUiRefreshElement("button", "source-reset-origin-btn", "Reset origin");
2416
+ sourceResetOriginBtn.type = "button";
2417
+ sourceResetOriginBtn.title = "Reset the editor origin and keep the current text in a new draft.";
2418
+ sourceResetOriginBtn.addEventListener("click", (event) => {
2419
+ event.preventDefault();
2420
+ event.stopPropagation();
2421
+ closeStudioUiRefreshMenus();
2422
+ resetEditorOrigin();
2423
+ });
2424
+ sourceOpenCurrentFileTabBtn = makeStudioUiRefreshElement("button", "source-open-file-tab-btn", "Open current file in new editor tab");
2425
+ sourceOpenCurrentFileTabBtn.type = "button";
2426
+ sourceOpenCurrentFileTabBtn.title = "Open this file-backed document in a new refreshable editor-only Studio tab.";
2427
+ sourceOpenCurrentFileTabBtn.addEventListener("click", (event) => {
2428
+ event.preventDefault();
2429
+ event.stopPropagation();
2430
+ const path = sourceState && sourceState.path ? String(sourceState.path) : "";
2431
+ if (!path) {
2432
+ setStatus("Open current file in new editor tab is only available for file-backed documents.", "warning");
2433
+ return;
2434
+ }
2435
+ closeStudioUiRefreshMenus();
2436
+ const targetUrl = buildAuthedStudioUrl("/", {
2437
+ mode: "editor-only",
2438
+ docSource: "file",
2439
+ docLabel: sourceState && sourceState.label ? sourceState.label : basenameForStudioPath(path),
2440
+ docPath: path,
2441
+ resourceDir: getCurrentResourceDirValue() || dirnameForDisplayPath(path),
2442
+ skipWorkspaceRestore: "1",
2443
+ });
2444
+ try {
2445
+ window.open(targetUrl, "_blank", "noopener");
2446
+ setStatus("Opening current file in a new editor tab.", "success");
2447
+ } catch (error) {
2448
+ setStatus((error && error.message) ? error.message : String(error || "Could not open file tab."), "warning");
2449
+ }
2450
+ });
2451
+ sourceOpenCurrentTextCopyTabBtn = makeStudioUiRefreshElement("button", "source-open-text-copy-tab-btn", "Open current text as copy in new editor tab");
2452
+ sourceOpenCurrentTextCopyTabBtn.type = "button";
2453
+ sourceOpenCurrentTextCopyTabBtn.title = "Open a detached copy of the current editor text in a new editor-only Studio tab.";
2454
+ sourceOpenCurrentTextCopyTabBtn.addEventListener("click", (event) => {
2455
+ event.preventDefault();
2456
+ event.stopPropagation();
2457
+ const content = String(sourceTextEl.value || "");
2458
+ if (!content.trim()) {
2459
+ setStatus("Editor is empty. Use New editor tab for a blank editor.", "warning");
2460
+ return;
2461
+ }
2462
+ closeStudioUiRefreshMenus();
2463
+ requestOpenEditorOnlyDocument(content, {
2464
+ label: sourceState && sourceState.label ? sourceState.label : "current editor",
2465
+ path: sourceState && sourceState.path ? sourceState.path : undefined,
2466
+ resourceDir: getCurrentResourceDirValue() || undefined,
2467
+ });
2468
+ });
2469
+ if (sendEditorBtn) sendEditorBtn.textContent = "Send current text to Pi editor";
2470
+ appendStudioUiRefreshMenuSection(contextMenu.menu, "Document", [sourceOriginSummaryEl, sourceResetOriginBtn, sourceOpenCurrentFileTabBtn, sourceOpenCurrentTextCopyTabBtn]);
2409
2471
  appendStudioUiRefreshMenuSection(contextMenu.menu, "Working directory", [resourceDirBtn, resourceDirLabel, resourceDirInputWrap]);
2472
+ if (!isEditorOnlyMode && sendEditorBtn) appendStudioUiRefreshMenuSection(contextMenu.menu, "Pi editor", [sendEditorBtn]);
2410
2473
  const cursorContextBtn = makeStudioUiRefreshElement("button", "completion-context-option", "Editor only");
2411
2474
  cursorContextBtn.type = "button";
2412
2475
  cursorContextBtn.setAttribute("data-completion-context-mode", "cursor");
@@ -2484,7 +2547,6 @@
2484
2547
  actionLineTwoEl.appendChild(copyDraftBtn);
2485
2548
  if (suggestCompletionBtn) actionLineTwoEl.appendChild(suggestCompletionBtn);
2486
2549
  if (openCompanionBtn) actionLineTwoEl.appendChild(openCompanionBtn);
2487
- if (!isEditorOnlyMode && sendEditorBtn) actionLineTwoEl.appendChild(sendEditorBtn);
2488
2550
  const replActionLineEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line repl-action-line");
2489
2551
  replActionLineEl.hidden = true;
2490
2552
  if (sendReplBtn) replActionLineEl.appendChild(sendReplBtn);
@@ -3213,13 +3275,26 @@
3213
3275
 
3214
3276
  function updateSourceBadge() {
3215
3277
  const label = sourceState && sourceState.label ? sourceState.label : "blank";
3216
- sourceBadgeEl.textContent = (studioUiRefreshEnabled ? "Origin: " : "Editor origin: ") + label + (hasRefreshableFilePath() ? " · file" : "");
3278
+ const originText = (studioUiRefreshEnabled ? "Origin: " : "Editor origin: ") + label + (hasRefreshableFilePath() ? " · file" : "");
3217
3279
  const descriptor = getCurrentStudioDocumentDescriptor();
3218
3280
  if (sourceBadgeEl) {
3281
+ sourceBadgeEl.textContent = originText;
3219
3282
  sourceBadgeEl.title = descriptor.fileBacked
3220
3283
  ? ("Editor origin: " + label + "\nClick to reset origin and detach the current editor text into a new draft. The file on disk will not be changed.")
3221
3284
  : ("Editor origin: " + label + "\nClick to reset origin and start a new independent draft while keeping the current text and local notes.");
3222
3285
  }
3286
+ if (sourceOriginSummaryEl) {
3287
+ sourceOriginSummaryEl.textContent = originText;
3288
+ sourceOriginSummaryEl.title = descriptor.fileBacked
3289
+ ? ("File-backed editor: " + (descriptor.path || label))
3290
+ : ("Detached editor origin: " + label);
3291
+ }
3292
+ if (sourceResetOriginBtn) {
3293
+ sourceResetOriginBtn.textContent = descriptor.fileBacked ? "Detach from file" : "Reset origin";
3294
+ sourceResetOriginBtn.title = descriptor.fileBacked
3295
+ ? "Detach the current editor text from this file and keep it in a new draft. The file on disk will not be changed."
3296
+ : "Reset the editor origin and keep the current text in a new draft.";
3297
+ }
3223
3298
  // Show "Set working dir" button when not file-backed
3224
3299
  var isFileBacked = hasRefreshableFilePath();
3225
3300
  if (isFileBacked) {
@@ -3560,6 +3635,17 @@
3560
3635
  setStatus("Right pane content focused.");
3561
3636
  }
3562
3637
 
3638
+ function switchRightPaneToPrimaryPreview() {
3639
+ const targetView = isEditorOnlyMode ? "editor-preview" : "preview";
3640
+ const snapshot = snapshotStudioScrollablePositions();
3641
+ setRightView(targetView);
3642
+ scheduleStudioScrollablePositionRestore(snapshot);
3643
+ const label = rightViewSelect && rightViewSelect.selectedOptions && rightViewSelect.selectedOptions[0]
3644
+ ? rightViewSelect.selectedOptions[0].textContent
3645
+ : (isEditorOnlyMode ? "Editor (Preview)" : "Response (Preview)");
3646
+ setStatus("Right pane view: " + String(label || "Preview") + ".");
3647
+ }
3648
+
3563
3649
  function cycleActivePaneView(direction) {
3564
3650
  if (activePane === "right") {
3565
3651
  if (!rightViewSelect || rightViewSelect.disabled) {
@@ -3858,6 +3944,16 @@
3858
3944
  return;
3859
3945
  }
3860
3946
 
3947
+ const isPreviewShortcut = (key.toLowerCase() === "p" || code === "KeyP")
3948
+ && (event.metaKey || event.ctrlKey)
3949
+ && event.altKey
3950
+ && !event.shiftKey;
3951
+ if (isPreviewShortcut) {
3952
+ event.preventDefault();
3953
+ switchRightPaneToPrimaryPreview();
3954
+ return;
3955
+ }
3956
+
3861
3957
  const isContentFocusShortcut = key === "F8" && !event.metaKey && !event.ctrlKey && !event.altKey;
3862
3958
  if (isContentFocusShortcut) {
3863
3959
  event.preventDefault();
@@ -9908,6 +10004,14 @@
9908
10004
 
9909
10005
  fileInput.disabled = uiBusy;
9910
10006
  if (sourceBadgeEl) sourceBadgeEl.disabled = uiBusy;
10007
+ if (sourceResetOriginBtn) sourceResetOriginBtn.disabled = uiBusy;
10008
+ if (sourceOpenCurrentFileTabBtn) {
10009
+ sourceOpenCurrentFileTabBtn.disabled = uiBusy || !hasRefreshableFilePath();
10010
+ sourceOpenCurrentFileTabBtn.title = hasRefreshableFilePath()
10011
+ ? "Open this file-backed document in a new refreshable editor-only Studio tab."
10012
+ : "Available after opening a file-backed document.";
10013
+ }
10014
+ if (sourceOpenCurrentTextCopyTabBtn) sourceOpenCurrentTextCopyTabBtn.disabled = uiBusy || wsState !== "Ready" || !String(sourceTextEl.value || "").trim();
9911
10015
  saveAsBtn.disabled = uiBusy;
9912
10016
  saveOverBtn.disabled = uiBusy || !canSaveOver;
9913
10017
  if (refreshFromDiskBtn) refreshFromDiskBtn.disabled = uiBusy || !canRefreshFromDisk;
@@ -10030,6 +10134,7 @@
10030
10134
  }
10031
10135
 
10032
10136
  function shouldRestorePersistedWorkspaceState(state) {
10137
+ if (skipInitialWorkspaceRestore) return false;
10033
10138
  if (!state || typeof state.text !== "string") return false;
10034
10139
  const storedSourceState = normalizeWorkspaceSourceState(state.sourceState);
10035
10140
  const initialIdentity = getWorkspaceStateIdentity(initialSourceState);
@@ -10951,6 +11056,7 @@
10951
11056
  else params.delete("docPath");
10952
11057
  if (nextDraftId) params.set("draftId", nextDraftId);
10953
11058
  else params.delete("draftId");
11059
+ params.delete("skipWorkspaceRestore");
10954
11060
  window.history.replaceState(null, "", currentUrl.toString());
10955
11061
  } catch {
10956
11062
  // Ignore URL-state update failures.
@@ -11166,10 +11272,10 @@
11166
11272
  appendPreviewLinkMenuButton(menu, "Open PDF preview", "open-pdf");
11167
11273
  appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
11168
11274
  } else if (kind === "text") {
11169
- appendPreviewLinkMenuButton(menu, "Open in new editor", "open-new");
11275
+ appendPreviewLinkMenuButton(menu, "Open file tab", "open-new");
11170
11276
  appendPreviewLinkMenuButton(menu, "Open here", "open-here");
11171
11277
  } else if (kind === "office") {
11172
- appendPreviewLinkMenuButton(menu, "Convert in new editor", "open-new");
11278
+ appendPreviewLinkMenuButton(menu, "Convert tab", "open-new");
11173
11279
  appendPreviewLinkMenuButton(menu, "Convert here", "open-here");
11174
11280
  } else if (kind === "image") {
11175
11281
  appendPreviewLinkMenuButton(menu, "Open image preview", "open-image");
@@ -11306,10 +11412,11 @@
11306
11412
  return;
11307
11413
  }
11308
11414
  const popup = pendingWindow || window.open("", "_blank");
11415
+ const openingLabel = getPreviewLocalLinkKind(href) === "office" ? "Opening converted document…" : "Opening file tab…";
11309
11416
  try {
11310
11417
  if (popup && popup.document && popup.document.body) {
11311
- popup.document.title = "Opening linked file…";
11312
- popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening linked file…</p>";
11418
+ popup.document.title = openingLabel;
11419
+ popup.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">" + escapeHtml(openingLabel) + "</p>";
11313
11420
  }
11314
11421
  } catch {}
11315
11422
  try {
@@ -11322,12 +11429,12 @@
11322
11429
  try {
11323
11430
  popup.opener = null;
11324
11431
  popup.location.href = targetUrl;
11325
- setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening linked file in a new editor.", "success");
11432
+ setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening file-backed document in a new editor.", "success");
11326
11433
  return;
11327
11434
  } catch {}
11328
11435
  }
11329
11436
  window.open(targetUrl, "_blank", "noopener");
11330
- setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening linked file in a new editor.", "success");
11437
+ setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening file-backed document in a new editor.", "success");
11331
11438
  } catch (error) {
11332
11439
  if (popup && !popup.closed) {
11333
11440
  try { popup.close(); } catch {}
@@ -18344,9 +18451,10 @@
18344
18451
  }
18345
18452
 
18346
18453
  if (message.type === "response_history") {
18454
+ const isTreeSync = message.reason === "tree";
18347
18455
  setResponseHistory(message.items, {
18348
- autoSelectLatest: followLatest,
18349
- preserveSelection: !followLatest,
18456
+ autoSelectLatest: isTreeSync ? true : followLatest,
18457
+ preserveSelection: isTreeSync ? true : !followLatest,
18350
18458
  silent: true,
18351
18459
  });
18352
18460
  return;
@@ -18502,11 +18610,11 @@
18502
18610
  const opened = navigatePendingCompanionWindow(responseRequestId, targetUrl);
18503
18611
  const readyMessage = typeof message.message === "string" && message.message.trim()
18504
18612
  ? message.message.trim()
18505
- : "Opened companion editor with a detached copy of the current editor text.";
18613
+ : "Opened editor tab with a detached copy of the current editor text.";
18506
18614
  setStatus(
18507
18615
  opened
18508
18616
  ? readyMessage
18509
- : (targetUrl ? "Companion editor ready: " + targetUrl : "Companion editor is ready, but Studio did not receive a URL."),
18617
+ : (targetUrl ? "Editor tab ready: " + targetUrl : "Editor tab is ready, but Studio did not receive a URL."),
18510
18618
  opened ? "success" : "warning",
18511
18619
  );
18512
18620
  return;
@@ -18826,8 +18934,8 @@
18826
18934
  try {
18827
18935
  companionWindow = window.open("", "_blank");
18828
18936
  if (companionWindow && companionWindow.document && companionWindow.document.body) {
18829
- companionWindow.document.title = "Opening companion editor…";
18830
- companionWindow.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening companion editor…</p>";
18937
+ companionWindow.document.title = "Opening editor tab…";
18938
+ companionWindow.document.body.innerHTML = "<p style=\"font: 13px -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; padding: 16px;\">Opening editor tab…</p>";
18831
18939
  }
18832
18940
  } catch {
18833
18941
  companionWindow = null;
@@ -18883,6 +18991,28 @@
18883
18991
  }
18884
18992
  }
18885
18993
 
18994
+ function requestOpenEditorOnlyDocument(content, options) {
18995
+ const requestId = beginUiAction("open_editor_only");
18996
+ if (!requestId) return false;
18997
+ openPendingCompanionWindow(requestId);
18998
+ const config = options && typeof options === "object" ? options : {};
18999
+ const sent = sendMessage({
19000
+ type: "open_editor_only_request",
19001
+ requestId,
19002
+ content: String(content || ""),
19003
+ label: config.label || "current editor",
19004
+ path: config.path || undefined,
19005
+ resourceDir: config.resourceDir || undefined,
19006
+ });
19007
+ if (!sent) {
19008
+ closePendingCompanionWindow(requestId);
19009
+ pendingRequestId = null;
19010
+ pendingKind = null;
19011
+ setBusy(false);
19012
+ }
19013
+ return sent;
19014
+ }
19015
+
18886
19016
  function describeSourceForAnnotation() {
18887
19017
  if (sourceState.source === "file" && sourceState.label) {
18888
19018
  return "file " + sourceState.label;
@@ -19498,27 +19628,10 @@
19498
19628
 
19499
19629
  if (openCompanionBtn) {
19500
19630
  openCompanionBtn.addEventListener("click", () => {
19501
- const content = sourceTextEl.value;
19502
-
19503
- const requestId = beginUiAction("open_editor_only");
19504
- if (!requestId) return;
19505
- openPendingCompanionWindow(requestId);
19506
-
19507
- const sent = sendMessage({
19508
- type: "open_editor_only_request",
19509
- requestId,
19510
- content,
19511
- label: sourceState && sourceState.label ? sourceState.label : "current editor",
19512
- path: sourceState && sourceState.path ? sourceState.path : undefined,
19631
+ requestOpenEditorOnlyDocument("", {
19632
+ label: "blank",
19513
19633
  resourceDir: getCurrentResourceDirValue() || undefined,
19514
19634
  });
19515
-
19516
- if (!sent) {
19517
- closePendingCompanionWindow(requestId);
19518
- pendingRequestId = null;
19519
- pendingKind = null;
19520
- setBusy(false);
19521
- }
19522
19635
  });
19523
19636
  }
19524
19637
 
@@ -20038,7 +20151,7 @@
20038
20151
  }
20039
20152
  if (sourceBadgeEl) {
20040
20153
  sourceBadgeEl.addEventListener("click", () => {
20041
- resetEditorOrigin();
20154
+ if (!studioUiRefreshEnabled) resetEditorOrigin();
20042
20155
  });
20043
20156
  }
20044
20157
  if (resourceDirBtn) {
package/client/studio.css CHANGED
@@ -5176,6 +5176,15 @@
5176
5176
  font-weight: 450;
5177
5177
  }
5178
5178
 
5179
+ body.studio-ui-refresh .studio-refresh-menu-item > .source-origin-summary {
5180
+ width: 100%;
5181
+ border-color: var(--border-subtle);
5182
+ background: var(--panel-2);
5183
+ color: var(--studio-info-text, var(--muted));
5184
+ white-space: normal;
5185
+ line-height: 1.35;
5186
+ }
5187
+
5179
5188
  body.studio-ui-refresh .studio-refresh-menu #critiqueBtn {
5180
5189
  justify-content: flex-start;
5181
5190
  text-align: left;
package/index.ts CHANGED
@@ -6991,7 +6991,7 @@ async function respondLocalPreviewLinkJson(req: IncomingMessage, res: ServerResp
6991
6991
  }
6992
6992
  const document = buildStudioLocalResourcePreviewDocument(resource);
6993
6993
  const docId = storeTransientStudioDocument(document);
6994
- const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId);
6994
+ const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
6995
6995
  const parsedUrl = new URL(url);
6996
6996
  respondJson(res, 200, {
6997
6997
  ...basePayload,
@@ -7056,7 +7056,7 @@ async function respondLocalPreviewLinkJson(req: IncomingMessage, res: ServerResp
7056
7056
  }
7057
7057
 
7058
7058
  const docId = storeTransientStudioDocument(document);
7059
- const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId);
7059
+ const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
7060
7060
  const parsedUrl = new URL(url);
7061
7061
  respondJson(res, 200, {
7062
7062
  ...basePayload,
@@ -9525,6 +9525,7 @@ function buildStudioUrl(
9525
9525
  mode: StudioUiMode = "full",
9526
9526
  doc?: InitialStudioDocument | null,
9527
9527
  docId?: string,
9528
+ options?: { skipWorkspaceRestore?: boolean },
9528
9529
  ): string {
9529
9530
  const params = new URLSearchParams({ token });
9530
9531
  if (mode !== "full") params.set("mode", mode);
@@ -9534,6 +9535,7 @@ function buildStudioUrl(
9534
9535
  if (doc?.path) params.set("docPath", doc.path);
9535
9536
  if (doc?.draftId) params.set("draftId", doc.draftId);
9536
9537
  if (doc?.resourceDir) params.set("resourceDir", doc.resourceDir);
9538
+ if (options?.skipWorkspaceRestore) params.set("skipWorkspaceRestore", "1");
9537
9539
  return `http://127.0.0.1:${port}/?${params.toString()}`;
9538
9540
  }
9539
9541
 
@@ -9994,8 +9996,8 @@ ${cssVarsBlock}
9994
9996
  <option value="cursor" selected>Context: editor only</option>
9995
9997
  <option value="session">Context: editor + latest response</option>
9996
9998
  </select>
9997
- <button id="openCompanionBtn" type="button" title="Open a detached copy of the current editor text in a new editor-only Studio tab.">New editor</button>
9998
- <button id="sendEditorBtn" type="button">Send to pi editor</button>
9999
+ <button id="openCompanionBtn" type="button" title="Open a blank editor-only Studio tab.">New editor tab</button>
10000
+ <button id="sendEditorBtn" type="button">Send current text to Pi editor</button>
9999
10001
  </div>
10000
10002
  <div class="source-actions-row">
10001
10003
  <button id="insertHeaderBtn" type="button" title="Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).">Annotation header</button>
@@ -10116,7 +10118,7 @@ ${cssVarsBlock}
10116
10118
  <div class="scratchpad-header">
10117
10119
  <div>
10118
10120
  <h2 id="reviewNotesTitle">Comments</h2>
10119
- <p class="scratchpad-description">Local comments for editor text and editor previews. They stay out of the text; source-anchored comments can be converted into inline <span class="review-notes-inline-token">[an: ...]</span> annotations.</p>
10121
+ <p class="scratchpad-description">Local comments for editor text and editor previews. They stay out of the text; can be converted into inline <span class="review-notes-inline-token">[an: ...]</span> annotations.</p>
10120
10122
  </div>
10121
10123
  <button id="reviewNotesCloseBtn" type="button" class="scratchpad-close-btn" aria-label="Hide comments" title="Hide comments">✕</button>
10122
10124
  </div>
@@ -10145,7 +10147,7 @@ ${cssVarsBlock}
10145
10147
  <section id="rightPane">
10146
10148
  <div id="rightSectionHeader" class="section-header">
10147
10149
  <div class="section-header-main">
10148
- <select id="rightViewSelect" aria-label="Response view mode" title="Right pane view mode. Shortcut: F7 when the right pane is active; F6 switches panes.">
10150
+ <select id="rightViewSelect" aria-label="Response view mode" title="Right pane view mode. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+P switches directly to Preview.">
10149
10151
  <option value="markdown">Response (Raw)</option>
10150
10152
  <option value="preview" selected>Response (Preview)</option>
10151
10153
  <option value="editor-preview">Editor (Preview)</option>
@@ -10237,6 +10239,7 @@ ${cssVarsBlock}
10237
10239
  <dl>
10238
10240
  <div><dt>F6</dt><dd>Switch between editor and right pane</dd></div>
10239
10241
  <div><dt>F7 / Shift+F7</dt><dd>Cycle the active pane's view</dd></div>
10242
+ <div><dt>Cmd/Ctrl+Alt+P</dt><dd>Switch the right pane directly to Preview</dd></div>
10240
10243
  <div><dt>F8</dt><dd>Focus editor text</dd></div>
10241
10244
  <div><dt>Shift+F8</dt><dd>Focus right-pane content</dd></div>
10242
10245
  <div><dt>F9</dt><dd>Toggle Zen mode</dd></div>
@@ -10932,10 +10935,11 @@ export default function (pi: ExtensionAPI) {
10932
10935
  };
10933
10936
  };
10934
10937
 
10935
- const broadcastResponseHistory = () => {
10938
+ const broadcastResponseHistory = (extra?: Record<string, unknown>) => {
10936
10939
  broadcast({
10937
10940
  type: "response_history",
10938
10941
  items: studioResponseHistory,
10942
+ ...(extra ?? {}),
10939
10943
  });
10940
10944
  };
10941
10945
 
@@ -11696,7 +11700,7 @@ export default function (pi: ExtensionAPI) {
11696
11700
  resourceDir,
11697
11701
  };
11698
11702
  const docId = storeTransientStudioDocument(document);
11699
- const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId);
11703
+ const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
11700
11704
  const parsedUrl = new URL(url);
11701
11705
  sendToClient(client, {
11702
11706
  type: "editor_only_ready",
@@ -11704,8 +11708,8 @@ export default function (pi: ExtensionAPI) {
11704
11708
  url,
11705
11709
  relativeUrl: `${parsedUrl.pathname}${parsedUrl.search}`,
11706
11710
  message: hasContent
11707
- ? "Companion editor is ready with a detached copy of the current editor text."
11708
- : "Blank companion editor is ready.",
11711
+ ? "Editor tab is ready with a detached copy of the current editor text."
11712
+ : "Blank editor tab is ready.",
11709
11713
  });
11710
11714
  return;
11711
11715
  }
@@ -13070,7 +13074,7 @@ export default function (pi: ExtensionAPI) {
13070
13074
  resourceDir: dirname(exportedPath),
13071
13075
  };
13072
13076
  const docId = storeTransientStudioDocument(document);
13073
- const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId);
13077
+ const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
13074
13078
  const parsedUrl = new URL(url);
13075
13079
  respondJson(res, 200, {
13076
13080
  ok: true,
@@ -13217,7 +13221,7 @@ export default function (pi: ExtensionAPI) {
13217
13221
  draftId: exportedPath ? undefined : createStudioDraftId(),
13218
13222
  };
13219
13223
  const docId = storeTransientStudioDocument(document);
13220
- const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId);
13224
+ const url = buildStudioUrl(serverState.port, serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true });
13221
13225
  const parsedUrl = new URL(url);
13222
13226
  respondJson(res, 200, {
13223
13227
  ok: true,
@@ -13838,13 +13842,32 @@ export default function (pi: ExtensionAPI) {
13838
13842
  });
13839
13843
 
13840
13844
 
13841
- pi.on("session_tree", async (_event, ctx) => {
13845
+ pi.on("session_tree", async (event, ctx) => {
13842
13846
  latestModelRequestCtx = ctx;
13843
- hydrateLatestAssistant(ctx.sessionManager.getBranch());
13847
+ const branchEntries = ctx.sessionManager.getBranch();
13848
+ hydrateLatestAssistant(branchEntries);
13844
13849
  refreshRuntimeMetadata({ cwd: ctx.cwd, model: ctx.model });
13845
13850
  refreshContextUsage(ctx);
13846
- broadcastResponseHistory();
13851
+ emitDebugEvent("session_tree", {
13852
+ oldLeafId: event.oldLeafId ?? null,
13853
+ newLeafId: event.newLeafId ?? null,
13854
+ branchEntryCount: branchEntries.length,
13855
+ responseHistoryCount: studioResponseHistory.length,
13856
+ });
13857
+ broadcastResponseHistory({
13858
+ reason: "tree",
13859
+ oldLeafId: event.oldLeafId ?? null,
13860
+ newLeafId: event.newLeafId ?? null,
13861
+ responseHistoryCount: studioResponseHistory.length,
13862
+ });
13847
13863
  broadcastState();
13864
+ broadcast({
13865
+ type: "info",
13866
+ level: "info",
13867
+ message: studioResponseHistory.length > 0
13868
+ ? "Pi session tree changed; Studio response history now follows the current branch. Editor text was left unchanged."
13869
+ : "Pi session tree changed; this branch has no assistant responses yet. Editor text was left unchanged.",
13870
+ });
13848
13871
  });
13849
13872
 
13850
13873
  pi.on("model_select", async (event, ctx) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.9.21",
3
+ "version": "0.9.23",
4
4
  "description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, active quiz, prompt/response history, live previews, and tmux-backed REPL/literate REPL workflows",
5
5
  "type": "module",
6
6
  "license": "MIT",