pi-studio 0.9.16 → 0.9.17

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
@@ -4,6 +4,13 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.17] — 2026-05-25
8
+
9
+ ### Changed
10
+ - Escape now dismisses a visible editor completion suggestion before falling through to other Studio Escape actions.
11
+ - Added a compact **Source & context** dropdown beside the editor-mode selector, with editor-only suggestions by default and an optional editor-plus-latest-response context mode; in-flight suggestion requests can now be stopped from the **Suggest** button.
12
+ - Kept Zen mode focused by hiding **Suggest** with the other secondary editor utilities while still showing **Send to REPL** controls when the REPL pane is active.
13
+
7
14
  ## [0.9.16] — 2026-05-24
8
15
 
9
16
  ### Added
package/README.md CHANGED
@@ -21,7 +21,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
21
21
  - Opens a two-pane browser workspace: **Editor** (left) + **Response/Working/Editor Preview** (right)
22
22
  - Supports one canonical full Studio view per Pi session, plus additional editor-only companion views when you just want extra editing/preview surfaces; the editor toolbar can open a detached copy of the current editor text as a companion view
23
23
  - Includes a global **Zen** mode for hiding secondary Studio chrome without changing the current left/right pane layout
24
- - Runs editor text directly, asks for structured critique (auto/writing/code focus), offers a manual **Suggest completion** action for short cursor-aware continuations (`Option/Alt+Tab` where available or `Cmd/Ctrl+Shift+Space` from the editor, `Tab` to insert a visible suggestion), or opens **Quiz me** for a Studio-native active-recall loop over the current editor text, selection, current file, folder, or repo, with optional focus guidance for shaping question selection
24
+ - Runs editor text directly, asks for structured critique (auto/writing/code focus), offers a manual **Suggest completion** action for short cursor-aware continuations (`Option/Alt+Tab` where available or `Cmd/Ctrl+Shift+Space` from the editor, `Tab` to insert a visible suggestion) with an optional editor-plus-latest-response context mode, or opens **Quiz me** for a Studio-native active-recall loop over the current editor text, selection, current file, folder, or repo, with optional focus guidance for shaping question selection
25
25
  - Includes a live **Working** view for following current model/tool activity, with `All` / `Thinking` / `Tools` filters, image previews for image-producing tool outputs, plus **Load visible into editor** and **Copy visible** actions; when cycling response history, Working follows saved working details for the selected response when available
26
26
  - Includes a right-pane **Files** view for browsing the current Pi session/resource directory, opening folders, loading text/code documents into the editor, previewing PDFs/images, copying paths, and revealing files in the file manager
27
27
  - Includes an optional tmux-backed **REPL** view for Shell, Python, IPython, Julia, R, GHCi, and Clojure sessions, with Raw/Literate send modes, `Cmd/Ctrl+Shift+Enter` **Send to REPL**, session start/stop/interrupt controls, a compact refresh-persistent **Studio REPL Record** of user and Pi-sent code, a secondary raw tmux mirror, agent-facing `studio_repl_status` / `studio_repl_send` tools, and Markdown/PDF/HTML export
@@ -115,6 +115,8 @@
115
115
  const replSendModeSelect = document.getElementById("replSendModeSelect");
116
116
  const copyDraftBtn = document.getElementById("copyDraftBtn");
117
117
  const suggestCompletionBtn = document.getElementById("suggestCompletionBtn");
118
+ const suggestCompletionOptionsBtn = document.getElementById("suggestCompletionOptionsBtn");
119
+ const completionContextSelect = document.getElementById("completionContextSelect");
118
120
  const completionSuggestionPanelEl = document.getElementById("completionSuggestionPanel");
119
121
  const completionSuggestionTextEl = document.getElementById("completionSuggestionText");
120
122
  const completionSuggestionInsertBtn = document.getElementById("completionSuggestionInsertBtn");
@@ -256,6 +258,8 @@
256
258
  const HTML_ARTIFACT_RESOURCE_FETCH_TIMEOUT_MS = 30_000;
257
259
  const EDITOR_TAB_TEXT = " ";
258
260
  const QUIZ_DEFAULT_COUNT = 5;
261
+ const COMPLETION_CONTEXT_STORAGE_KEY = "piStudio.completionContextMode";
262
+ const COMPLETION_CONTEXT_MAX_CHARS = 12000;
259
263
  const QUIZ_SCOPES = ["editor", "selection", "file", "folder", "repo"];
260
264
  const QUIZ_ANGLES = ["general", "scientist", "mathematician", "statistician", "developer", "reviewer"];
261
265
  const QUIZ_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
@@ -1949,6 +1953,7 @@
1949
1953
  let editorLanguage = "markdown";
1950
1954
  let responseHighlightEnabled = false;
1951
1955
  let completionSuggestionState = null;
1956
+ let completionSuggestionContextMode = readCompletionSuggestionContextMode();
1952
1957
  let completionSuggestionInFlight = false;
1953
1958
  let completionSuggestionRequestId = null;
1954
1959
  let completionSuggestionPendingSnapshot = null;
@@ -2339,6 +2344,34 @@
2339
2344
  appendStudioUiRefreshMenuSection(reviewMenu.menu, "Setting", [lensSelect]);
2340
2345
  }
2341
2346
 
2347
+ let contextMenu = null;
2348
+ if (suggestCompletionOptionsBtn) {
2349
+ suggestCompletionOptionsBtn.hidden = false;
2350
+ if (completionContextSelect) completionContextSelect.hidden = true;
2351
+ contextMenu = makeStudioUiRefreshMenu(suggestCompletionOptionsBtn, "context", "studio-refresh-context-anchor");
2352
+ if (sourceBadgeEl) appendStudioUiRefreshMenuSection(contextMenu.menu, "Document", [sourceBadgeEl]);
2353
+ appendStudioUiRefreshMenuSection(contextMenu.menu, "Working directory", [resourceDirBtn, resourceDirLabel, resourceDirInputWrap]);
2354
+ const cursorContextBtn = makeStudioUiRefreshElement("button", "completion-context-option", "Editor only");
2355
+ cursorContextBtn.type = "button";
2356
+ cursorContextBtn.setAttribute("data-completion-context-mode", "cursor");
2357
+ const sessionContextBtn = makeStudioUiRefreshElement("button", "completion-context-option", "Editor + latest response");
2358
+ sessionContextBtn.type = "button";
2359
+ sessionContextBtn.setAttribute("data-completion-context-mode", "session");
2360
+ [cursorContextBtn, sessionContextBtn].forEach((button) => {
2361
+ button.addEventListener("click", (event) => {
2362
+ event.preventDefault();
2363
+ event.stopPropagation();
2364
+ setCompletionSuggestionContextMode(button.getAttribute("data-completion-context-mode") === "session" ? "session" : "cursor");
2365
+ syncActionButtons();
2366
+ });
2367
+ });
2368
+ appendStudioUiRefreshMenuSection(contextMenu.menu, "Suggestions", [cursorContextBtn, sessionContextBtn]);
2369
+ if (syncBadgeEl) {
2370
+ syncBadgeEl.hidden = false;
2371
+ appendStudioUiRefreshMenuSection(contextMenu.menu, "Status", [syncBadgeEl]);
2372
+ }
2373
+ }
2374
+
2342
2375
  const headerTopEl = makeStudioUiRefreshElement("div", "studio-refresh-header-top");
2343
2376
  const titleGroupEl = makeStudioUiRefreshElement("div", "studio-refresh-title-group");
2344
2377
  if (leftFocusBtn) {
@@ -2351,6 +2384,10 @@
2351
2384
  } else if (editorViewSelect) {
2352
2385
  titleGroupEl.appendChild(editorViewSelect);
2353
2386
  }
2387
+ if (contextMenu) {
2388
+ titleGroupEl.appendChild(makeStudioUiRefreshSeparator());
2389
+ titleGroupEl.appendChild(contextMenu.anchor);
2390
+ }
2354
2391
  headerTopEl.appendChild(titleGroupEl);
2355
2392
  const headerToolsEl = makeStudioUiRefreshElement("div", "studio-refresh-pane-tools");
2356
2393
  if (reviewNotesBtn) headerToolsEl.appendChild(reviewNotesBtn);
@@ -2359,18 +2396,7 @@
2359
2396
  if (reviewMenu) headerToolsEl.appendChild(reviewMenu.anchor);
2360
2397
  headerTopEl.appendChild(headerToolsEl);
2361
2398
 
2362
- const headerUtilityEl = makeStudioUiRefreshElement("div", "studio-refresh-header-utility");
2363
- const utilityLeftEl = makeStudioUiRefreshElement("div", "studio-refresh-utility-left");
2364
- if (sourceBadgeEl) utilityLeftEl.appendChild(sourceBadgeEl);
2365
- if (sourceBadgeEl && (resourceDirBtn || resourceDirLabel || resourceDirInputWrap || syncBadgeEl)) {
2366
- utilityLeftEl.appendChild(makeStudioUiRefreshSeparator());
2367
- }
2368
- if (resourceDirBtn) utilityLeftEl.appendChild(resourceDirBtn);
2369
- if (resourceDirLabel) utilityLeftEl.appendChild(resourceDirLabel);
2370
- if (resourceDirInputWrap) utilityLeftEl.appendChild(resourceDirInputWrap);
2371
- if (syncBadgeEl) utilityLeftEl.appendChild(syncBadgeEl);
2372
- headerUtilityEl.appendChild(utilityLeftEl);
2373
- leftHeaderEl.replaceChildren(headerTopEl, headerUtilityEl);
2399
+ leftHeaderEl.replaceChildren(headerTopEl);
2374
2400
 
2375
2401
  const rightHeaderEl = document.getElementById("rightSectionHeader");
2376
2402
  if (rightHeaderEl && rightViewSelect) {
@@ -2402,18 +2428,18 @@
2402
2428
  const actionLineOneEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
2403
2429
  if (!isEditorOnlyMode && sendRunBtn) actionLineOneEl.appendChild(sendRunBtn);
2404
2430
  if (!isEditorOnlyMode && queueSteerBtn) actionLineOneEl.appendChild(queueSteerBtn);
2405
- const replActionLineEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line repl-action-line");
2406
- replActionLineEl.hidden = true;
2407
- if (!isEditorOnlyMode && sendReplBtn) replActionLineEl.appendChild(sendReplBtn);
2408
- if (!isEditorOnlyMode && replSendModeSelect) replActionLineEl.appendChild(replSendModeSelect);
2409
- const actionLineTwoEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
2431
+ const actionLineTwoEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line studio-refresh-utility-action-line");
2410
2432
  actionLineTwoEl.appendChild(copyDraftBtn);
2411
2433
  if (suggestCompletionBtn) actionLineTwoEl.appendChild(suggestCompletionBtn);
2412
2434
  if (openCompanionBtn) actionLineTwoEl.appendChild(openCompanionBtn);
2413
2435
  if (!isEditorOnlyMode && sendEditorBtn) actionLineTwoEl.appendChild(sendEditorBtn);
2436
+ const replActionLineEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line repl-action-line");
2437
+ replActionLineEl.hidden = true;
2438
+ if (!isEditorOnlyMode && sendReplBtn) replActionLineEl.appendChild(sendReplBtn);
2439
+ if (!isEditorOnlyMode && replSendModeSelect) replActionLineEl.appendChild(replSendModeSelect);
2414
2440
  if (actionLineOneEl.childNodes.length > 0) actionsEl.appendChild(actionLineOneEl);
2415
- if (replActionLineEl.childNodes.length > 0) actionsEl.appendChild(replActionLineEl);
2416
2441
  actionsEl.appendChild(actionLineTwoEl);
2442
+ if (replActionLineEl.childNodes.length > 0) actionsEl.appendChild(replActionLineEl);
2417
2443
 
2418
2444
  const stateEl = makeStudioUiRefreshElement("div", "studio-refresh-toolbar-state");
2419
2445
  const annotationsButton = makeStudioUiRefreshElement("button", "", "Annotations");
@@ -2435,7 +2461,9 @@
2435
2461
  annotationsButton,
2436
2462
  viewButton,
2437
2463
  reviewButton: reviewMenu ? reviewMenu.button : null,
2438
- menus: [annotationsMenu, viewMenu].concat(reviewMenu ? [reviewMenu] : []),
2464
+ menus: [annotationsMenu, viewMenu]
2465
+ .concat(contextMenu ? [contextMenu] : [])
2466
+ .concat(reviewMenu ? [reviewMenu] : []),
2439
2467
  };
2440
2468
 
2441
2469
  document.addEventListener("click", (event) => {
@@ -3691,6 +3719,14 @@
3691
3719
  return;
3692
3720
  }
3693
3721
 
3722
+ if (plainEscape && completionSuggestionState) {
3723
+ event.preventDefault();
3724
+ hideCompletionSuggestion();
3725
+ focusSourceTextNoScroll();
3726
+ setStatus("Dismissed completion suggestion.");
3727
+ return;
3728
+ }
3729
+
3694
3730
  if (handleCompletionSuggestionAcceptKey(event)) return;
3695
3731
 
3696
3732
  if ((key === "?" || (key === "/" && event.shiftKey)) && !event.metaKey && !event.ctrlKey && !event.altKey && !isTextEntryShortcutTarget(event.target)) {
@@ -4244,20 +4280,15 @@
4244
4280
 
4245
4281
  if (isEditorOnlyMode) {
4246
4282
  syncBadgeEl.hidden = true;
4247
- syncBadgeEl.classList.remove("sync");
4248
- return;
4249
- }
4250
-
4251
- if (rightView === "trace") {
4252
- syncBadgeEl.hidden = true;
4253
- syncBadgeEl.classList.remove("sync");
4283
+ syncBadgeEl.textContent = "Editor-only tab";
4284
+ syncBadgeEl.classList.remove("sync", "out-of-sync");
4254
4285
  return;
4255
4286
  }
4256
4287
 
4257
4288
  if (!latestResponseHasContent) {
4258
- syncBadgeEl.hidden = true;
4259
- syncBadgeEl.textContent = "In sync with response";
4260
- syncBadgeEl.classList.remove("sync");
4289
+ syncBadgeEl.hidden = false;
4290
+ syncBadgeEl.textContent = "No latest response";
4291
+ syncBadgeEl.classList.remove("sync", "out-of-sync");
4261
4292
  return;
4262
4293
  }
4263
4294
 
@@ -4265,15 +4296,10 @@
4265
4296
  ? normalizedEditorText
4266
4297
  : normalizeForCompare(sourceTextEl.value);
4267
4298
  const inSync = normalizedEditor === latestResponseNormalized;
4268
- syncBadgeEl.hidden = !inSync;
4269
- syncBadgeEl.textContent = "In sync with response";
4270
-
4271
- if (inSync) {
4272
- syncBadgeEl.classList.add("sync");
4273
- return;
4274
- }
4275
-
4276
- syncBadgeEl.classList.remove("sync");
4299
+ syncBadgeEl.hidden = false;
4300
+ syncBadgeEl.textContent = inSync ? "In sync with response" : "Editor differs from latest response";
4301
+ syncBadgeEl.classList.toggle("sync", inSync);
4302
+ syncBadgeEl.classList.toggle("out-of-sync", !inSync);
4277
4303
  }
4278
4304
 
4279
4305
  function buildPlainMarkdownHtml(markdown, options) {
@@ -8667,9 +8693,14 @@
8667
8693
  syncRunAndCritiqueButtons();
8668
8694
  copyDraftBtn.disabled = uiBusy;
8669
8695
  if (suggestCompletionBtn) {
8670
- suggestCompletionBtn.disabled = uiBusy || completionSuggestionInFlight || wsState !== "Ready" || !String(sourceTextEl.value || "").trim();
8671
- suggestCompletionBtn.textContent = completionSuggestionInFlight ? "Suggesting…" : "Suggest";
8672
- }
8696
+ suggestCompletionBtn.disabled = wsState !== "Ready" || (!completionSuggestionInFlight && (uiBusy || !String(sourceTextEl.value || "").trim()));
8697
+ suggestCompletionBtn.textContent = completionSuggestionInFlight ? "Stop" : "Suggest";
8698
+ suggestCompletionBtn.title = completionSuggestionInFlight
8699
+ ? "Stop the current suggestion request."
8700
+ : "Ask the current model for a short completion at the editor cursor. Shortcut: Option/Alt+Tab where available, or Cmd/Ctrl+Shift+Space from the editor.";
8701
+ }
8702
+ if (suggestCompletionOptionsBtn) suggestCompletionOptionsBtn.disabled = uiBusy || completionSuggestionInFlight;
8703
+ syncCompletionSuggestionContextUi();
8673
8704
  if (openCompanionBtn) openCompanionBtn.disabled = uiBusy || wsState !== "Ready";
8674
8705
  if (highlightSelect) highlightSelect.disabled = uiBusy;
8675
8706
  if (lineNumbersSelect) lineNumbersSelect.disabled = uiBusy;
@@ -9003,6 +9034,69 @@
9003
9034
  }
9004
9035
  }
9005
9036
 
9037
+ function readCompletionSuggestionContextMode() {
9038
+ try {
9039
+ const stored = window.localStorage ? String(window.localStorage.getItem(COMPLETION_CONTEXT_STORAGE_KEY) || "") : "";
9040
+ return stored === "session" ? "session" : "cursor";
9041
+ } catch {
9042
+ return "cursor";
9043
+ }
9044
+ }
9045
+
9046
+ function setCompletionSuggestionContextMode(mode) {
9047
+ completionSuggestionContextMode = mode === "session" ? "session" : "cursor";
9048
+ if (completionContextSelect) completionContextSelect.value = completionSuggestionContextMode;
9049
+ try {
9050
+ if (window.localStorage) window.localStorage.setItem(COMPLETION_CONTEXT_STORAGE_KEY, completionSuggestionContextMode);
9051
+ } catch {}
9052
+ setStatus(completionSuggestionContextMode === "session"
9053
+ ? "Suggestions will include the latest response as context."
9054
+ : "Suggestions will use cursor-local editor context only.");
9055
+ }
9056
+
9057
+ function syncCompletionSuggestionContextUi() {
9058
+ if (completionContextSelect) completionContextSelect.value = completionSuggestionContextMode;
9059
+ if (suggestCompletionOptionsBtn) {
9060
+ suggestCompletionOptionsBtn.textContent = "Source & context";
9061
+ suggestCompletionOptionsBtn.title = completionSuggestionContextMode === "session"
9062
+ ? "Document source, working directory, status, and suggestion context. Suggestions include editor plus latest response."
9063
+ : "Document source, working directory, status, and suggestion context. Suggestions use editor-only context.";
9064
+ suggestCompletionOptionsBtn.setAttribute("aria-label", suggestCompletionOptionsBtn.title);
9065
+ }
9066
+ document.querySelectorAll("[data-completion-context-mode]").forEach((button) => {
9067
+ if (!(button instanceof HTMLElement)) return;
9068
+ const mode = button.getAttribute("data-completion-context-mode") === "session" ? "session" : "cursor";
9069
+ const selected = mode === completionSuggestionContextMode;
9070
+ button.classList.toggle("is-selected", selected);
9071
+ button.setAttribute("aria-pressed", selected ? "true" : "false");
9072
+ button.textContent = (selected ? "✓ " : " ") + (mode === "session" ? "Editor + latest response" : "Editor only");
9073
+ });
9074
+ }
9075
+
9076
+ function trimCompletionContextText(text) {
9077
+ const value = String(text || "").trim();
9078
+ if (value.length <= COMPLETION_CONTEXT_MAX_CHARS) return value;
9079
+ return value.slice(value.length - COMPLETION_CONTEXT_MAX_CHARS);
9080
+ }
9081
+
9082
+ function getCompletionSuggestionContextText() {
9083
+ if (completionSuggestionContextMode !== "session") return "";
9084
+ const selected = getSelectedHistoryItem ? getSelectedHistoryItem() : null;
9085
+ const responseText = selected && typeof selected.markdown === "string" && selected.markdown.trim()
9086
+ ? selected.markdown
9087
+ : latestResponseMarkdown;
9088
+ const parts = [];
9089
+ if (selected && typeof selected.promptTriggerText === "string" && selected.promptTriggerText.trim()) {
9090
+ parts.push("Latest request/steering:\n" + trimCompletionContextText(selected.promptTriggerText));
9091
+ } else if (selected && typeof selected.prompt === "string" && selected.prompt.trim()) {
9092
+ parts.push("Latest prompt:\n" + trimCompletionContextText(selected.prompt));
9093
+ }
9094
+ if (String(responseText || "").trim()) {
9095
+ parts.push("Latest response:\n" + trimCompletionContextText(responseText));
9096
+ }
9097
+ return trimCompletionContextText(parts.join("\n\n---\n\n"));
9098
+ }
9099
+
9006
9100
  function hideCompletionSuggestion() {
9007
9101
  completionSuggestionState = null;
9008
9102
  if (completionSuggestionTextEl) completionSuggestionTextEl.textContent = "";
@@ -9073,10 +9167,29 @@
9073
9167
  || Boolean(completionSuggestionPanelEl && activeEl instanceof Element && completionSuggestionPanelEl.contains(activeEl));
9074
9168
  }
9075
9169
 
9170
+ function cancelCompletionSuggestion() {
9171
+ if (!completionSuggestionInFlight || !completionSuggestionRequestId) {
9172
+ setStatus("No suggestion request is running.", "warning");
9173
+ return;
9174
+ }
9175
+ setStatus("Stopping suggestion…", "warning");
9176
+ const sent = sendMessage({
9177
+ type: "completion_suggestion_cancel_request",
9178
+ requestId: completionSuggestionRequestId,
9179
+ });
9180
+ if (!sent) {
9181
+ completionSuggestionInFlight = false;
9182
+ completionSuggestionRequestId = null;
9183
+ completionSuggestionPendingSnapshot = null;
9184
+ completionSuggestionRefocusEditorOnResult = false;
9185
+ syncActionButtons();
9186
+ }
9187
+ }
9188
+
9076
9189
  function requestCompletionSuggestion() {
9077
9190
  if (isEditorOnlyMode && !sourceTextEl) return;
9078
9191
  if (completionSuggestionInFlight) {
9079
- setStatus("Suggestion request already in progress.", "warning");
9192
+ cancelCompletionSuggestion();
9080
9193
  return;
9081
9194
  }
9082
9195
  const text = String(sourceTextEl.value || "");
@@ -9086,6 +9199,7 @@
9086
9199
  }
9087
9200
  const selectionStart = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : text.length;
9088
9201
  const selectionEnd = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : selectionStart;
9202
+ const contextText = getCompletionSuggestionContextText();
9089
9203
  const requestId = makeRequestId();
9090
9204
  completionSuggestionInFlight = true;
9091
9205
  completionSuggestionRequestId = requestId;
@@ -9103,6 +9217,8 @@
9103
9217
  language: editorLanguage || "",
9104
9218
  label: sourceState && sourceState.label ? sourceState.label : "Studio editor",
9105
9219
  path: sourceState && sourceState.path ? sourceState.path : undefined,
9220
+ contextMode: completionSuggestionContextMode,
9221
+ contextText: contextText || undefined,
9106
9222
  });
9107
9223
  if (!sent) {
9108
9224
  completionSuggestionInFlight = false;
@@ -17947,6 +18063,13 @@
17947
18063
  requestCompletionSuggestion();
17948
18064
  });
17949
18065
  }
18066
+ if (completionContextSelect) {
18067
+ completionContextSelect.value = completionSuggestionContextMode;
18068
+ completionContextSelect.addEventListener("change", () => {
18069
+ setCompletionSuggestionContextMode(completionContextSelect.value);
18070
+ syncActionButtons();
18071
+ });
18072
+ }
17950
18073
  if (completionSuggestionInsertBtn) {
17951
18074
  completionSuggestionInsertBtn.addEventListener("click", () => {
17952
18075
  insertCompletionSuggestion();
package/client/studio.css CHANGED
@@ -4336,7 +4336,8 @@
4336
4336
  }
4337
4337
 
4338
4338
  body.studio-ui-refresh .studio-refresh-header-utility {
4339
- grid-template-columns: minmax(0, 1fr) auto;
4339
+ grid-template-columns: minmax(0, 1fr);
4340
+ justify-items: start;
4340
4341
  }
4341
4342
 
4342
4343
  body.studio-ui-refresh .studio-refresh-pane-identity {
@@ -4353,12 +4354,18 @@
4353
4354
  flex-wrap: nowrap;
4354
4355
  }
4355
4356
 
4356
- body.studio-ui-refresh .studio-refresh-context-group,
4357
- body.studio-ui-refresh .studio-refresh-utility-left {
4357
+ body.studio-ui-refresh .studio-refresh-context-group {
4358
4358
  overflow: hidden;
4359
4359
  white-space: nowrap;
4360
4360
  }
4361
4361
 
4362
+ body.studio-ui-refresh .studio-refresh-utility-left {
4363
+ overflow: visible;
4364
+ white-space: nowrap;
4365
+ justify-content: flex-start;
4366
+ justify-self: start;
4367
+ }
4368
+
4362
4369
  body.studio-ui-refresh .studio-refresh-title-group {
4363
4370
  position: relative;
4364
4371
  z-index: 2;
@@ -4462,12 +4469,14 @@
4462
4469
  border-color: transparent;
4463
4470
  background: transparent;
4464
4471
  padding: 3px 5px;
4465
- font-size: 13px;
4472
+ font-size: 12px;
4473
+ font-weight: 400;
4474
+ line-height: 1.25;
4466
4475
  border-radius: 8px;
4467
4476
  }
4468
4477
 
4469
4478
  body.studio-ui-refresh #sourceBadge {
4470
- color: var(--text);
4479
+ color: var(--studio-info-text, var(--muted));
4471
4480
  max-width: min(34rem, 54vw);
4472
4481
  min-width: 0;
4473
4482
  overflow: hidden;
@@ -4475,7 +4484,6 @@
4475
4484
  white-space: nowrap;
4476
4485
  }
4477
4486
 
4478
- body.studio-ui-refresh #resourceDirBtn,
4479
4487
  body.studio-ui-refresh #reviewNotesBtn,
4480
4488
  body.studio-ui-refresh #outlineBtn,
4481
4489
  body.studio-ui-refresh #scratchpadBtn,
@@ -4483,11 +4491,19 @@
4483
4491
  color: var(--text);
4484
4492
  }
4485
4493
 
4494
+ body.studio-ui-refresh #resourceDirBtn,
4486
4495
  body.studio-ui-refresh #resourceDirLabel {
4487
4496
  color: var(--studio-info-text, var(--muted));
4488
4497
  font-weight: 400;
4489
4498
  }
4490
4499
 
4500
+ body.studio-ui-refresh #resourceDirBtn:hover,
4501
+ body.studio-ui-refresh #resourceDirLabel:hover {
4502
+ color: var(--text);
4503
+ background: var(--studio-header-action-hover-bg, var(--panel-2));
4504
+ border-color: transparent;
4505
+ }
4506
+
4491
4507
  body.studio-ui-refresh #resourceDirInputWrap.visible {
4492
4508
  display: inline-flex;
4493
4509
  }
@@ -4501,9 +4517,11 @@
4501
4517
  border-radius: 999px;
4502
4518
  padding: 2px 7px;
4503
4519
  background: transparent;
4504
- color: var(--muted);
4505
- opacity: 0.82;
4506
- font-weight: 450;
4520
+ color: var(--studio-info-text, var(--muted));
4521
+ opacity: 1;
4522
+ font-size: 12px;
4523
+ font-weight: 400;
4524
+ line-height: 1.25;
4507
4525
  }
4508
4526
 
4509
4527
  body.studio-ui-refresh #syncBadge[hidden] {
@@ -4519,6 +4537,11 @@
4519
4537
  opacity: 0.72;
4520
4538
  }
4521
4539
 
4540
+ body.studio-ui-refresh #syncBadge.out-of-sync::before {
4541
+ background: var(--warning, var(--accent));
4542
+ opacity: 0.95;
4543
+ }
4544
+
4522
4545
  body.studio-ui-refresh #reviewNotesBtn,
4523
4546
  body.studio-ui-refresh #outlineBtn,
4524
4547
  body.studio-ui-refresh #scratchpadBtn,
@@ -4536,9 +4559,10 @@
4536
4559
  body.studio-zen-mode #exportPdfBtn,
4537
4560
  body.studio-zen-mode .studio-refresh-tool-tab,
4538
4561
  body.studio-zen-mode .studio-refresh-toolbar-state,
4539
- body.studio-zen-mode .studio-refresh-toolbar-actions .studio-refresh-action-line:nth-child(n+3),
4562
+ body.studio-zen-mode .studio-refresh-toolbar-actions .studio-refresh-utility-action-line,
4540
4563
  body.studio-zen-mode .source-actions-row:nth-child(n+3),
4541
4564
  body.studio-zen-mode #copyDraftBtn,
4565
+ body.studio-zen-mode #suggestCompletionBtn,
4542
4566
  body.studio-zen-mode #openCompanionBtn,
4543
4567
  body.studio-zen-mode #sendEditorBtn {
4544
4568
  display: none !important;
@@ -4805,6 +4829,19 @@
4805
4829
  width: min(320px, calc(100vw - 48px));
4806
4830
  }
4807
4831
 
4832
+ body.studio-ui-refresh .studio-refresh-title-group .studio-refresh-context-anchor .studio-refresh-chip {
4833
+ color: var(--studio-info-text, var(--muted));
4834
+ font-size: 13px;
4835
+ font-weight: 500;
4836
+ padding: 3px 5px;
4837
+ }
4838
+
4839
+ body.studio-ui-refresh .studio-refresh-context-anchor .studio-refresh-menu {
4840
+ left: 0;
4841
+ right: auto;
4842
+ width: min(360px, calc(100vw - 48px));
4843
+ }
4844
+
4808
4845
  body.studio-ui-refresh .studio-refresh-menu[hidden] {
4809
4846
  display: none !important;
4810
4847
  }
@@ -4852,6 +4889,11 @@
4852
4889
  text-align: left;
4853
4890
  }
4854
4891
 
4892
+ body.studio-ui-refresh .completion-context-option.is-selected {
4893
+ color: var(--text);
4894
+ font-weight: 650;
4895
+ }
4896
+
4855
4897
  body.studio-ui-refresh .studio-refresh-menu-item > button:not(:disabled):hover,
4856
4898
  body.studio-ui-refresh .studio-refresh-menu-item > select:not(:disabled):hover {
4857
4899
  background: var(--studio-header-action-hover-bg, var(--panel-2));
package/index.ts CHANGED
@@ -320,6 +320,13 @@ interface CompletionSuggestionRequestMessage {
320
320
  language?: string;
321
321
  label?: string;
322
322
  path?: string;
323
+ contextMode?: "cursor" | "session";
324
+ contextText?: string;
325
+ }
326
+
327
+ interface CompletionSuggestionCancelRequestMessage {
328
+ type: "completion_suggestion_cancel_request";
329
+ requestId: string;
323
330
  }
324
331
 
325
332
  interface QuizGenerateRequestMessage {
@@ -463,6 +470,7 @@ type IncomingStudioMessage =
463
470
  | AnnotationRequestMessage
464
471
  | SendRunRequestMessage
465
472
  | CompletionSuggestionRequestMessage
473
+ | CompletionSuggestionCancelRequestMessage
466
474
  | QuizGenerateRequestMessage
467
475
  | QuizAnswerRequestMessage
468
476
  | QuizDiscussRequestMessage
@@ -485,6 +493,7 @@ type IncomingStudioMessage =
485
493
  const REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
486
494
  const PREVIEW_RENDER_MAX_CHARS = 400_000;
487
495
  const STUDIO_COMPLETION_MAX_TEXT_CHARS = 250_000;
496
+ const STUDIO_COMPLETION_MAX_CONTEXT_CHARS = 12_000;
488
497
  const STUDIO_COMPLETION_PREFIX_CHARS = 12_000;
489
498
  const STUDIO_COMPLETION_SUFFIX_CHARS = 6_000;
490
499
  const PDF_EXPORT_MAX_CHARS = 400_000;
@@ -7217,6 +7226,8 @@ function buildStudioCompletionSuggestionPrompt(options: {
7217
7226
  language?: string;
7218
7227
  label?: string;
7219
7228
  path?: string;
7229
+ contextMode?: "cursor" | "session";
7230
+ contextText?: string;
7220
7231
  }): string {
7221
7232
  const text = String(options.text || "");
7222
7233
  const start = Math.max(0, Math.min(Math.floor(options.selectionStart || 0), text.length));
@@ -7226,17 +7237,23 @@ function buildStudioCompletionSuggestionPrompt(options: {
7226
7237
  const suffix = text.slice(end, Math.min(text.length, end + STUDIO_COMPLETION_SUFFIX_CHARS));
7227
7238
  const language = String(options.language || "").trim() || "unknown";
7228
7239
  const label = String(options.label || options.path || "Studio editor").trim();
7240
+ const contextText = String(options.contextText || "").trim().slice(-STUDIO_COMPLETION_MAX_CONTEXT_CHARS);
7229
7241
  return [
7230
7242
  "Generate an inline completion for the current editor cursor position.",
7231
7243
  "Return only the exact text to insert. Do not wrap it in Markdown fences. Do not explain.",
7232
7244
  "Match the surrounding language, style, indentation, and register.",
7233
7245
  "Keep the suggestion short unless the context clearly asks for a longer continuation.",
7246
+ contextText
7247
+ ? "Use the extra session context only as background. Do not continue the extra context directly unless the editor cursor calls for it."
7248
+ : "Use only the cursor-local editor context below.",
7234
7249
  selected
7235
7250
  ? "The selected text will be replaced by the completion."
7236
7251
  : "The completion will be inserted at the cursor.",
7237
7252
  "",
7238
7253
  `File/context label: ${label}`,
7239
7254
  `Language mode: ${language}`,
7255
+ `Suggestion context mode: ${contextText ? "editor plus latest response" : "editor only"}`,
7256
+ contextText ? ["", "<extra_context>", contextText, "</extra_context>"].join("\n") : "",
7240
7257
  "",
7241
7258
  "<prefix>",
7242
7259
  prefix,
@@ -7262,6 +7279,9 @@ async function runStudioCompletionSuggestion(ctx: StudioModelRequestContext, opt
7262
7279
  language?: string;
7263
7280
  label?: string;
7264
7281
  path?: string;
7282
+ contextMode?: "cursor" | "session";
7283
+ contextText?: string;
7284
+ signal?: AbortSignal;
7265
7285
  }): Promise<string> {
7266
7286
  const prompt = buildStudioCompletionSuggestionPrompt(options);
7267
7287
  // Intentionally omit `reasoning`: pi-ai treats absent reasoning as off/disabled
@@ -7271,6 +7291,7 @@ async function runStudioCompletionSuggestion(ctx: StudioModelRequestContext, opt
7271
7291
  maxTokens: 650,
7272
7292
  timeoutMs: 60_000,
7273
7293
  trim: false,
7294
+ signal: options.signal,
7274
7295
  }));
7275
7296
  if (!suggestion.trim()) throw new Error("Model returned an empty completion suggestion.");
7276
7297
  return suggestion;
@@ -7685,12 +7706,20 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
7685
7706
  };
7686
7707
  }
7687
7708
 
7709
+ if (msg.type === "completion_suggestion_cancel_request" && typeof msg.requestId === "string") {
7710
+ return {
7711
+ type: "completion_suggestion_cancel_request",
7712
+ requestId: msg.requestId,
7713
+ };
7714
+ }
7715
+
7688
7716
  if (msg.type === "completion_suggestion_request" && typeof msg.requestId === "string" && typeof msg.text === "string") {
7689
7717
  const textLength = msg.text.length;
7690
7718
  const rawStart = typeof msg.selectionStart === "number" && Number.isFinite(msg.selectionStart) ? msg.selectionStart : textLength;
7691
7719
  const rawEnd = typeof msg.selectionEnd === "number" && Number.isFinite(msg.selectionEnd) ? msg.selectionEnd : rawStart;
7692
7720
  const selectionStart = Math.max(0, Math.min(Math.floor(rawStart), textLength));
7693
7721
  const selectionEnd = Math.max(selectionStart, Math.min(Math.floor(rawEnd), textLength));
7722
+ const contextMode = msg.contextMode === "session" ? "session" : "cursor";
7694
7723
  return {
7695
7724
  type: "completion_suggestion_request",
7696
7725
  requestId: msg.requestId,
@@ -7700,6 +7729,8 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
7700
7729
  language: typeof msg.language === "string" ? msg.language : undefined,
7701
7730
  label: typeof msg.label === "string" ? msg.label : undefined,
7702
7731
  path: typeof msg.path === "string" ? msg.path : undefined,
7732
+ contextMode,
7733
+ contextText: contextMode === "session" && typeof msg.contextText === "string" ? msg.contextText.slice(-STUDIO_COMPLETION_MAX_CONTEXT_CHARS) : undefined,
7703
7734
  };
7704
7735
  }
7705
7736
 
@@ -9582,6 +9613,11 @@ ${cssVarsBlock}
9582
9613
  <div class="source-actions-row">
9583
9614
  <button id="copyDraftBtn" type="button" title="Copy the current editor text to the clipboard.">Copy</button>
9584
9615
  <button id="suggestCompletionBtn" type="button" title="Ask the current model for a short completion at the editor cursor. Shortcut: Option/Alt+Tab where available, or Cmd/Ctrl+Shift+Space from the editor.">Suggest</button>
9616
+ <button id="suggestCompletionOptionsBtn" type="button" hidden title="Suggestion context options">▾</button>
9617
+ <select id="completionContextSelect" hidden aria-label="Suggestion context mode" title="Choose how much context Suggest includes.">
9618
+ <option value="cursor" selected>Context: editor only</option>
9619
+ <option value="session">Context: editor + latest response</option>
9620
+ </select>
9585
9621
  <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>
9586
9622
  <button id="sendEditorBtn" type="button">Send to pi editor</button>
9587
9623
  </div>
@@ -9844,6 +9880,7 @@ ${cssVarsBlock}
9844
9880
  <div><dt>Cmd/Ctrl+Enter</dt><dd>Run editor text, or queue steering during an active run</dd></div>
9845
9881
  <div><dt>Option/Alt+Tab or Cmd/Ctrl+Shift+Space</dt><dd>Suggest a completion at the editor cursor</dd></div>
9846
9882
  <div><dt>Tab</dt><dd>Insert a visible completion suggestion; otherwise indent selected editor text</dd></div>
9883
+ <div><dt>Esc</dt><dd>Dismiss a visible completion suggestion, close overlays, exit pane focus, or stop an active request</dd></div>
9847
9884
  <div><dt>Shift+Tab</dt><dd>Unindent selected editor text</dd></div>
9848
9885
  </dl>
9849
9886
  </section>
@@ -9938,6 +9975,7 @@ export default function (pi: ExtensionAPI) {
9938
9975
  let studioReplActiveSessionName: string | null = null;
9939
9976
  let compactInProgress = false;
9940
9977
  let compactRequestId: string | null = null;
9978
+ const activeCompletionSuggestions = new Map<string, AbortController>();
9941
9979
 
9942
9980
  const selectStudioReplSessionForTool = (params: { sessionName?: string; target?: string }): { session: StudioReplSessionInfo | null; error?: string; sessions: StudioReplSessionInfo[] } => {
9943
9981
  const state = listStudioReplSessions();
@@ -11424,6 +11462,21 @@ export default function (pi: ExtensionAPI) {
11424
11462
  return;
11425
11463
  }
11426
11464
 
11465
+ if (msg.type === "completion_suggestion_cancel_request") {
11466
+ if (!isValidRequestId(msg.requestId)) {
11467
+ sendToClient(client, { type: "completion_suggestion_error", requestId: msg.requestId, message: "Invalid request ID." });
11468
+ return;
11469
+ }
11470
+ const controller = activeCompletionSuggestions.get(msg.requestId);
11471
+ if (!controller) {
11472
+ sendToClient(client, { type: "completion_suggestion_error", requestId: msg.requestId, message: "No matching suggestion request is running." });
11473
+ return;
11474
+ }
11475
+ controller.abort();
11476
+ sendToClient(client, { type: "completion_suggestion_progress", requestId: msg.requestId, message: "Stopping suggestion…" });
11477
+ return;
11478
+ }
11479
+
11427
11480
  if (msg.type === "completion_suggestion_request") {
11428
11481
  if (!isValidRequestId(msg.requestId)) {
11429
11482
  sendToClient(client, { type: "completion_suggestion_error", requestId: msg.requestId, message: "Invalid request ID." });
@@ -11443,6 +11496,8 @@ export default function (pi: ExtensionAPI) {
11443
11496
  return;
11444
11497
  }
11445
11498
  sendToClient(client, { type: "completion_suggestion_progress", requestId: msg.requestId, message: "Generating suggestion…" });
11499
+ const completionController = new AbortController();
11500
+ activeCompletionSuggestions.set(msg.requestId, completionController);
11446
11501
  void (async () => {
11447
11502
  try {
11448
11503
  const suggestion = await runStudioCompletionSuggestion(ctx, {
@@ -11452,6 +11507,9 @@ export default function (pi: ExtensionAPI) {
11452
11507
  language: msg.language,
11453
11508
  label: msg.label,
11454
11509
  path: msg.path,
11510
+ contextMode: msg.contextMode,
11511
+ contextText: msg.contextText,
11512
+ signal: completionController.signal,
11455
11513
  });
11456
11514
  sendToClient(client, {
11457
11515
  type: "completion_suggestion_result",
@@ -11464,8 +11522,12 @@ export default function (pi: ExtensionAPI) {
11464
11522
  sendToClient(client, {
11465
11523
  type: "completion_suggestion_error",
11466
11524
  requestId: msg.requestId,
11467
- message: `Suggestion failed: ${error instanceof Error ? error.message : String(error)}`,
11525
+ message: completionController.signal.aborted
11526
+ ? "Suggestion stopped."
11527
+ : `Suggestion failed: ${error instanceof Error ? error.message : String(error)}`,
11468
11528
  });
11529
+ } finally {
11530
+ activeCompletionSuggestions.delete(msg.requestId);
11469
11531
  }
11470
11532
  })();
11471
11533
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.9.16",
3
+ "version": "0.9.17",
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",