pi-studio 0.9.54 → 0.9.55

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,15 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.55] — 2026-09-02
8
+
9
+ ### Changed
10
+ - Let side questions use **X-high** and **Max** thinking when the active model supports them, while retaining **Low** as the default.
11
+
12
+ ### Fixed
13
+ - Open watched previews launched from Files or local Studio links with the preview pane focused, matching `/studio --watch`, and label the exposed raw source clearly as read-only and disk-following.
14
+ - Normalize Studio buttons and dropdown select controls with flat browser-neutral styling and consistent chevrons instead of WebKit's native shaded controls.
15
+
7
16
  ## [0.9.54] — 2026-09-02
8
17
 
9
18
  ### Added
package/README.md CHANGED
@@ -118,6 +118,7 @@ Open **Review → Side question**, select **Side questions** in the right pane,
118
118
  - **Also use files from** controls the read-only boundary available to the side agent: no other files, the document folder, the current repository, or another chosen folder. Related-file access requires an allowed folder for the current Studio session; if the selected root is not already allowed, Studio names the canonical folder and asks before starting the thread. An exact-file grant never exposes its parent folder.
119
119
  - With **Repository** selected, **Include Git context** can capture status, staged and unstaged tracked-file changes, and up to 20 recent commit summaries. The bounded snapshot is frozen when the thread starts and exposed through fixed read-only tools; untracked contents remain available only through the existing root-confined file reader.
120
120
  - **Include the current main conversation snapshot** is opt-in. Side questions and answers otherwise never enter the main Pi history; **Bring to main conversation** is the explicit handoff.
121
+ - **Thinking** defaults to **Low** for quick asides. **X-high** and **Max** appear when the active model advertises support, for complex side questions where the extra latency is worthwhile.
121
122
  - Local context tools can map filenames, search readable text, and read selected ranges. Studio revalidates the allowed context root before prompts and built-in local tool calls; symlinks, root replacement, and traversal outside that root are rejected. PDF extraction uses `pdftotext`; DOCX, ODT, and EPUB extraction uses Pandoc.
122
123
  - **Allow web search** is opt-in and appears when `BRAVE_API_KEY` is available to the Pi process. Model-chosen search queries are sent to Brave Search; the side agent is instructed not to copy private local passages into queries. Web answers cite result URLs and identify when they rely on search-result snippets rather than full page content.
123
124
  - **Additional Pi tools** lists eligible tools already registered by the user's installed Pi extensions. Selection is explicit, remembered locally, and frozen when the thread starts. Studio reloads only the extensions owning the selected tools into the isolated side runtime and activates only those tool names; it does not import or depend on any particular third-party extension. Gateway tools are labelled and require an additional confirmation because selecting one may expose further services configured behind it.
@@ -151,7 +152,7 @@ caption: Optional caption
151
152
 
152
153
  To view an existing PDF directly, run `/studio report.pdf` (or `/studio "path with spaces/report.pdf"`). The PDF opens read-only in a focused companion preview, even when the full Studio workspace is already open. A `#page=N` suffix selects the initial page. Auto-refresh is off by default; use `/studio --watch report.pdf` to start with it on, or toggle **Auto-refresh** in the PDF card or focused viewer. Studio checks only while the tab is visible and waits for two matching file-version observations before reloading, which avoids reading a PDF while LaTeX is still writing it. **Cmd/Ctrl+Alt+R** manually refreshes the focused or visible PDF. This is distinct from `/studio-pdf`, which exports Markdown, LaTeX, code, or the last response to a new PDF.
153
154
 
154
- For a disk-authoritative text or code preview, run `/studio --watch notes.md` or choose **Preview file (follow changes)** from a local-link menu / **Preview (follow)** in Files. This opens a transient read-only companion tab rather than an editable buffer. Studio polls the canonical file, debounces change bursts, compares SHA-256 content revisions, accepts ordinary and atomic-replacement saves, and never writes back. Each watcher socket is bound to the exact path in a bounded, expiring server-created tab capability; if that identity expires before a reload or reconnect, the tab stays read-only and asks you to open a fresh watched preview. A missing, unreadable, oversized, binary, or path-replaced source leaves the last good preview visible and automatically recovers when the same canonical file becomes readable again. Render failures likewise retain the last successfully staged document rather than replacing it with partial output. On successful updates, Studio restores a nearby stable content anchor where possible and falls back to the previous scroll ratio.
155
+ For a disk-authoritative text or code preview, run `/studio --watch notes.md` or choose **Preview file (follow changes)** from a local-link menu / **Preview (follow)** in Files. This opens a transient read-only companion tab with the preview pane focused; leaving pane focus reveals the same disk-following text as **Source** with a visible **Read-only · follows disk** badge rather than as an editable buffer. Studio polls the canonical file, debounces change bursts, compares SHA-256 content revisions, accepts ordinary and atomic-replacement saves, and never writes back. Each watcher socket is bound to the exact path in a bounded, expiring server-created tab capability; if that identity expires before a reload or reconnect, the tab stays read-only and asks you to open a fresh watched preview. A missing, unreadable, oversized, binary, or path-replaced source leaves the last good preview visible and automatically recovers when the same canonical file becomes readable again. Render failures likewise retain the last successfully staged document rather than replacing it with partial output. On successful updates, Studio restores a nearby stable content anchor where possible and falls back to the previous scroll ratio.
155
156
 
156
157
  ### Mermaid icons
157
158
 
package/ROADMAP.md CHANGED
@@ -61,6 +61,14 @@ Builds on the resource-location foundation:
61
61
  - add **Cmd/Ctrl+Shift+S** for Save As and keep autosave opt-in rather than default;
62
62
  - carry the resulting disk identity and revision model forward into `StudioBufferStore`.
63
63
 
64
+ ## 0.9.55 — Watched-preview and control polish (shipped 2026-09-02)
65
+
66
+ A browser-neutral UX pass before buffer-first work:
67
+
68
+ - focus followed previews on their rendered pane and label the exposed source as read-only and disk-following;
69
+ - normalize Studio buttons and dropdown controls across Chromium and WebKit without changing native select interaction;
70
+ - expose X-high and Max side-question thinking only when supported by the active model, while retaining Low as the default.
71
+
64
72
  ## 0.10.0 — Buffer-first editing
65
73
 
66
74
  The first architectural `0.10` release should add:
@@ -74,7 +74,9 @@
74
74
  : null;
75
75
  const referenceBadgeEl = document.getElementById("referenceBadge");
76
76
  const editorViewSelect = document.getElementById("editorViewSelect");
77
+ const editorViewSelectWrap = document.getElementById("editorViewSelectWrap");
77
78
  const rightViewSelect = document.getElementById("rightViewSelect");
79
+ const rightViewSelectWrap = document.getElementById("rightViewSelectWrap");
78
80
  const followSelect = document.getElementById("followSelect");
79
81
  const responseHighlightSelect = document.getElementById("responseHighlightSelect");
80
82
  const responseFontSizeSelect = document.getElementById("responseFontSizeSelect");
@@ -486,6 +488,7 @@
486
488
  const EDITOR_TAB_TEXT = " ";
487
489
  const QUIZ_DEFAULT_COUNT = 5;
488
490
  const SIDE_QUESTION_THINKING_STORAGE_KEY = "piStudio.sideQuestionThinking";
491
+ const SIDE_QUESTION_THINKING_LEVELS = Object.freeze(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
489
492
  const SIDE_QUESTION_GATHER_STORAGE_KEY = "piStudio.sideQuestionGatherScope";
490
493
  const SIDE_QUESTION_TOOLS_STORAGE_KEY = "piStudio.sideQuestionTools";
491
494
  const SIDE_QUESTION_TRANSCRIPT_RENDER_MAX_CHARS = 400_000;
@@ -496,6 +499,7 @@
496
499
  const QUIZ_ANGLES = ["general", "scientist", "mathematician", "statistician", "developer", "reviewer"];
497
500
  const QUIZ_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
498
501
  let sideQuestionState = null;
502
+ let sideQuestionThinkingLevels = [...SIDE_QUESTION_THINKING_LEVELS];
499
503
  let sideQuestionWebSearchAvailable = false;
500
504
  let sideQuestionAvailablePiTools = [];
501
505
  let sideQuestionPreviewRenderNonce = 0;
@@ -524,11 +528,12 @@
524
528
  thinking: (() => {
525
529
  try {
526
530
  const value = window.localStorage && window.localStorage.getItem(SIDE_QUESTION_THINKING_STORAGE_KEY);
527
- return ["off", "minimal", "low", "medium", "high"].includes(value) ? value : "low";
531
+ return SIDE_QUESTION_THINKING_LEVELS.includes(value) ? value : "low";
528
532
  } catch { return "low"; }
529
533
  })(),
530
534
  draft: "",
531
535
  };
536
+ let sideQuestionPreferredThinking = sideQuestionUi.thinking;
532
537
  let quizOverlayEl = null;
533
538
  let quizDialogEl = null;
534
539
  let quizPreviewRenderNonce = 0;
@@ -2914,9 +2919,28 @@
2914
2919
  }
2915
2920
  titleGroupEl.appendChild(makeStudioUiRefreshSeparator());
2916
2921
  if (isEditorOnlyMode) {
2917
- titleGroupEl.appendChild(makeStudioUiRefreshElement("span", "studio-refresh-static-title", "Editor (Raw)"));
2922
+ const staticTitleEl = makeStudioUiRefreshElement(
2923
+ "span",
2924
+ "studio-refresh-static-title",
2925
+ isWatchedFilePreview ? "Source" : "Editor (Raw)",
2926
+ );
2927
+ if (isWatchedFilePreview) {
2928
+ staticTitleEl.classList.add("studio-watched-source-title");
2929
+ staticTitleEl.setAttribute("aria-label", "Source, read-only. Follows the file on disk.");
2930
+ staticTitleEl.title = "This source is read-only and follows the file on disk.";
2931
+ }
2932
+ titleGroupEl.appendChild(staticTitleEl);
2933
+ if (isWatchedFilePreview) {
2934
+ const readOnlyBadgeEl = makeStudioUiRefreshElement(
2935
+ "span",
2936
+ "studio-watched-source-read-only-badge",
2937
+ "Read-only · follows disk",
2938
+ );
2939
+ readOnlyBadgeEl.setAttribute("aria-hidden", "true");
2940
+ titleGroupEl.appendChild(readOnlyBadgeEl);
2941
+ }
2918
2942
  } else if (editorViewSelect) {
2919
- titleGroupEl.appendChild(editorViewSelect);
2943
+ titleGroupEl.appendChild(editorViewSelectWrap || editorViewSelect);
2920
2944
  }
2921
2945
  if (contextMenu) {
2922
2946
  titleGroupEl.appendChild(makeStudioUiRefreshSeparator());
@@ -2942,7 +2966,7 @@
2942
2966
  rightTitleGroupEl.appendChild(rightFocusBtn);
2943
2967
  rightTitleGroupEl.appendChild(makeStudioUiRefreshSeparator());
2944
2968
  }
2945
- rightTitleGroupEl.appendChild(rightViewSelect);
2969
+ rightTitleGroupEl.appendChild(rightViewSelectWrap || rightViewSelect);
2946
2970
  rightIdentityEl.appendChild(rightTitleGroupEl);
2947
2971
  const rightToolsEl = makeStudioUiRefreshElement("div", "studio-refresh-pane-tools");
2948
2972
  if (watchedOpenEditableBtn && isWatchedFilePreview) rightToolsEl.appendChild(watchedOpenEditableBtn);
@@ -3615,6 +3639,9 @@
3615
3639
  if (typeof message.thinkingLevel === "string") {
3616
3640
  piThinkingLevel = message.thinkingLevel.trim();
3617
3641
  }
3642
+ if (Array.isArray(message.sideQuestionThinkingLevels)) {
3643
+ applySideQuestionThinkingLevels(message.sideQuestionThinkingLevels);
3644
+ }
3618
3645
  renderFooterModelMenu();
3619
3646
  }
3620
3647
 
@@ -3649,8 +3676,8 @@
3649
3676
  });
3650
3677
  footerModelMenuEl.innerHTML = ""
3651
3678
  + "<div class='footer-model-menu-heading'>Pi model & thinking</div>"
3652
- + "<label class='footer-model-menu-field'><span>Pi model</span><select id='footerPiModelSelect'>" + modelOptionsHtml.join("") + "</select></label>"
3653
- + "<label class='footer-model-menu-field'><span>Thinking</span><select id='footerPiThinkingSelect'>" + thinkingOptionsHtml.join("") + "</select></label>"
3679
+ + "<label class='footer-model-menu-field'><span>Pi model</span><span class='studio-menu-select-wrap'><select id='footerPiModelSelect'>" + modelOptionsHtml.join("") + "</select></span></label>"
3680
+ + "<label class='footer-model-menu-field'><span>Thinking</span><span class='studio-menu-select-wrap'><select id='footerPiThinkingSelect'>" + thinkingOptionsHtml.join("") + "</select></span></label>"
3654
3681
  + "<div class='footer-model-menu-note'>Affects future Pi turns. Studio Suggest has its own model setting.</div>";
3655
3682
  }
3656
3683
 
@@ -3730,7 +3757,7 @@
3730
3757
  footerThemeMenuEl.innerHTML = ""
3731
3758
  + "<div class='footer-model-menu-heading'>Pi theme</div>"
3732
3759
  + (optionsHtml.length
3733
- ? "<label class='footer-model-menu-field'><span>Active theme</span><select id='footerPiThemeSelect'>" + optionsHtml.join("") + "</select></label>"
3760
+ ? "<label class='footer-model-menu-field'><span>Active theme</span><span class='studio-menu-select-wrap'><select id='footerPiThemeSelect'>" + optionsHtml.join("") + "</select></span></label>"
3734
3761
  : "<div class='footer-model-menu-note'>No Pi themes are available yet.</div>")
3735
3762
  + "<div class='footer-model-menu-note'>Switches the active Pi theme and persists it to Pi settings.</div>";
3736
3763
  }
@@ -4359,7 +4386,9 @@
4359
4386
  ].forEach(([btn, pane]) => {
4360
4387
  if (!btn) return;
4361
4388
  const isFocusedPane = paneFocusTarget === pane;
4362
- const paneName = pane === "right" ? "response" : "editor";
4389
+ const paneName = pane === "right"
4390
+ ? (isWatchedFilePreview ? "watched preview" : "response")
4391
+ : (isWatchedFilePreview ? "read-only source" : "editor");
4363
4392
  btn.classList.toggle("is-active", isFocusedPane);
4364
4393
  btn.setAttribute("aria-pressed", isFocusedPane ? "true" : "false");
4365
4394
  btn.textContent = isFocusedPane ? "Exit focus" : "Focus pane";
@@ -11616,10 +11645,10 @@
11616
11645
  return "<div class='repl-panel'>"
11617
11646
  + "<div class='repl-toolbar'>"
11618
11647
  + "<div class='repl-controls'>"
11619
- + "<label class='repl-control-label'>Runtime <select data-repl-runtime aria-label='REPL runtime'>" + runtimeOptions + "</select></label>"
11648
+ + "<label class='repl-control-label'>Runtime <select class='studio-flat-select' data-repl-runtime aria-label='REPL runtime'>" + runtimeOptions + "</select></label>"
11620
11649
  + "<label class='repl-control-label repl-command-label'>Start cmd <input data-repl-command type='text' value='" + escapeHtml(replCommand) + "' placeholder='default' aria-label='REPL start command' title='Command used by Start for this runtime. Leave blank for the default; use this for envs, e.g. .venv/bin/python, uv run python, or conda run --no-capture-output -n env python.'></label>"
11621
11650
  + "<button class='repl-start-btn' type='button' data-repl-action='start'" + (replBusy || replTmuxAvailable === false ? " disabled" : "") + " title='Start or switch to a " + escapeHtml(runtimeLabel) + " session using the selected runtime and start command.'>Start</button>"
11622
- + "<label class='repl-control-label repl-session-label'>Session <select data-repl-session aria-label='REPL session'" + (visibleSessions.length ? "" : " disabled") + ">" + sessionOptions + "</select></label>"
11651
+ + "<label class='repl-control-label repl-session-label'>Session <select class='studio-flat-select' data-repl-session aria-label='REPL session'" + (visibleSessions.length ? "" : " disabled") + ">" + sessionOptions + "</select></label>"
11623
11652
  + "<details class='repl-more-controls'>"
11624
11653
  + "<summary title='More REPL actions'>More</summary>"
11625
11654
  + "<div class='repl-more-menu'>"
@@ -11845,7 +11874,7 @@
11845
11874
  const options = getFileBrowserSortOptions().map((option) => {
11846
11875
  return "<option value='" + escapeHtml(option.value) + "'" + (option.value === currentSort ? " selected" : "") + ">" + escapeHtml(option.label) + "</option>";
11847
11876
  }).join("");
11848
- return "<select class='files-sort-select' data-files-sort aria-label='Files sort order' title='Sort file browser entries. Folders remain grouped first.'>" + options + "</select>";
11877
+ return "<select class='files-sort-select studio-flat-select' data-files-sort aria-label='Files sort order' title='Sort file browser entries. Folders remain grouped first.'>" + options + "</select>";
11849
11878
  }
11850
11879
 
11851
11880
  function buildFileBrowserEntryRowHtml(entry) {
@@ -11900,7 +11929,7 @@
11900
11929
  const label = String((location && location.label) || basenameForStudioPath(path) || path);
11901
11930
  return "<option value='" + escapeHtml(path) + "'" + (path === rootDir ? " selected" : "") + ">" + escapeHtml(label + " — " + path) + "</option>";
11902
11931
  }).join("");
11903
- return "<select class='files-location-select' data-files-location aria-label='Allowed Files location' title='Choose a folder allowed for this Studio session on the computer running Pi.'>" + options + "</select>";
11932
+ return "<select class='files-location-select studio-flat-select' data-files-location aria-label='Allowed Files location' title='Choose a folder allowed for this Studio session on the computer running Pi.'>" + options + "</select>";
11904
11933
  }
11905
11934
 
11906
11935
  function buildFileBrowserPanelHtml() {
@@ -12990,6 +13019,49 @@
12990
13019
  return values.map(([value, label]) => "<option value='" + escapeHtml(value) + "'" + (value === current ? " selected" : "") + ">" + escapeHtml(label) + "</option>").join("");
12991
13020
  }
12992
13021
 
13022
+ function clampSideQuestionThinkingLevel(value, levels) {
13023
+ const available = Array.isArray(levels) && levels.length ? levels : [...SIDE_QUESTION_THINKING_LEVELS];
13024
+ if (available.includes(value)) return value;
13025
+ const requestedIndex = SIDE_QUESTION_THINKING_LEVELS.indexOf(value);
13026
+ if (requestedIndex < 0) return available.includes("low") ? "low" : available[0];
13027
+ for (let index = requestedIndex; index < SIDE_QUESTION_THINKING_LEVELS.length; index += 1) {
13028
+ const candidate = SIDE_QUESTION_THINKING_LEVELS[index];
13029
+ if (available.includes(candidate)) return candidate;
13030
+ }
13031
+ for (let index = requestedIndex - 1; index >= 0; index -= 1) {
13032
+ const candidate = SIDE_QUESTION_THINKING_LEVELS[index];
13033
+ if (available.includes(candidate)) return candidate;
13034
+ }
13035
+ return "off";
13036
+ }
13037
+
13038
+ function applySideQuestionThinkingLevels(value) {
13039
+ const seen = new Set();
13040
+ const normalized = (Array.isArray(value) ? value : []).filter((level) => {
13041
+ if (typeof level !== "string" || !SIDE_QUESTION_THINKING_LEVELS.includes(level) || seen.has(level)) return false;
13042
+ seen.add(level);
13043
+ return true;
13044
+ });
13045
+ const next = normalized.length ? normalized : [...SIDE_QUESTION_THINKING_LEVELS];
13046
+ const levelsChanged = next.length !== sideQuestionThinkingLevels.length
13047
+ || next.some((level, index) => level !== sideQuestionThinkingLevels[index]);
13048
+ sideQuestionThinkingLevels = next;
13049
+ const nextThinking = clampSideQuestionThinkingLevel(sideQuestionPreferredThinking, next);
13050
+ const thinkingChanged = nextThinking !== sideQuestionUi.thinking;
13051
+ if (thinkingChanged) sideQuestionUi.thinking = nextThinking;
13052
+ if ((levelsChanged || thinkingChanged) && rightView === "side-questions" && (!sideQuestionState || !sideQuestionState.threadId)) {
13053
+ renderSideQuestionView();
13054
+ }
13055
+ }
13056
+
13057
+ function getSideQuestionThinkingOptions() {
13058
+ return sideQuestionThinkingLevels.map((level) => {
13059
+ if (level === "xhigh") return [level, "X-high (slower)"];
13060
+ if (level === "max") return [level, "Max (slowest)"];
13061
+ return [level, level.charAt(0).toUpperCase() + level.slice(1)];
13062
+ });
13063
+ }
13064
+
12993
13065
  function renderSideQuestionPiToolPicker() {
12994
13066
  const selected = new Set(sideQuestionUi.toolIds);
12995
13067
  const selectedCount = sideQuestionUi.toolIds.length;
@@ -13026,15 +13098,15 @@
13026
13098
  return "<div class='side-question-empty'>"
13027
13099
  + "<div class='side-question-intro'><h2>Side question</h2><p>Ask something without adding it to the main Pi conversation. Choose the starting text and whether Studio may look through related files.</p></div>"
13028
13100
  + "<div class='side-question-context-grid'>"
13029
- + "<label>Starting text<select data-side-question-field='focusMode' aria-describedby='sideQuestionContextRule'>" + sideQuestionSelectOptions([
13101
+ + "<label>Starting text<select class='studio-flat-select' data-side-question-field='focusMode' aria-describedby='sideQuestionContextRule'>" + sideQuestionSelectOptions([
13030
13102
  ["auto", "Automatic"], ["selection", "Editor selection only"], ["section", "Heading block or nearby text at cursor"], ["editor", "Whole editor document"], ["response", "Displayed response"], ["none", "No starting text"],
13031
13103
  ], sideQuestionUi.focusMode) + "</select></label>"
13032
- + "<label>Also use files from<select data-side-question-field='gatherScope'>" + sideQuestionSelectOptions([
13104
+ + "<label>Also use files from<select class='studio-flat-select' data-side-question-field='gatherScope'>" + sideQuestionSelectOptions([
13033
13105
  ["none", "No other files"], ["folder", "Same folder as document"], ["repo", "Repository"], ["custom", "Choose a folder"],
13034
13106
  ], scope) + "</select></label>"
13035
- + "<label>Thinking<select data-side-question-field='thinking'>" + sideQuestionSelectOptions([
13036
- ["off", "Off"], ["minimal", "Minimal"], ["low", "Low"], ["medium", "Medium"], ["high", "High"],
13037
- ], sideQuestionUi.thinking) + "</select></label>"
13107
+ + "<label>Thinking<select class='studio-flat-select' data-side-question-field='thinking'>" + sideQuestionSelectOptions(
13108
+ getSideQuestionThinkingOptions(), sideQuestionUi.thinking
13109
+ ) + "</select></label>"
13038
13110
  + "</div>"
13039
13111
  + "<details class='side-question-context-rule'><summary id='sideQuestionContextRule'>Automatic: selection → heading block at cursor → nearby text</summary><p>A heading block starts at the nearest Markdown/LaTeX heading above the cursor and ends before the next heading of the same or higher level. With no heading, Studio uses the surrounding text block or a nearby excerpt.</p></details>"
13040
13112
  + (scope === "none" ? "" : "<p class='side-question-context-access-note'>Related-file access is limited to folders allowed for this Studio session. Studio asks before starting if this folder is not already allowed.</p>")
@@ -13392,9 +13464,10 @@
13392
13464
  if (sideQuestionUi.gatherScope !== "repo") sideQuestionUi.gitContext = false;
13393
13465
  try { if (window.localStorage) window.localStorage.setItem(SIDE_QUESTION_GATHER_STORAGE_KEY, sideQuestionUi.gatherScope); } catch {}
13394
13466
  }
13395
- if (field === "thinking") {
13467
+ if (field === "thinking" && sideQuestionThinkingLevels.includes(target.value)) {
13468
+ sideQuestionPreferredThinking = target.value;
13396
13469
  sideQuestionUi.thinking = target.value;
13397
- try { if (window.localStorage) window.localStorage.setItem(SIDE_QUESTION_THINKING_STORAGE_KEY, sideQuestionUi.thinking); } catch {}
13470
+ try { if (window.localStorage) window.localStorage.setItem(SIDE_QUESTION_THINKING_STORAGE_KEY, sideQuestionPreferredThinking); } catch {}
13398
13471
  }
13399
13472
  if (field === "includeConversation") sideQuestionUi.includeConversation = target.checked;
13400
13473
  if (field === "gitContext") sideQuestionUi.gitContext = target.checked && getSideQuestionGatherScope() === "repo";
@@ -16321,13 +16394,13 @@
16321
16394
  return "<div class='studio-quiz-setup'>"
16322
16395
  + "<p class='studio-quiz-copy'>A short active-recall loop: answer one question, check it, ask about the card if useful, then move on.</p>"
16323
16396
  + "<div class='studio-quiz-fields'>"
16324
- + "<label>Scope<select data-quiz-field='scope'>"
16397
+ + "<label>Scope<select class='studio-flat-select' data-quiz-field='scope'>"
16325
16398
  + QUIZ_SCOPES.map((candidate) => candidate === "selection" && !hasSelection ? "" : renderQuizOption(candidate, scope, getQuizScopeLabel(candidate))).join("")
16326
16399
  + "</select></label>"
16327
- + "<label>Angle<select data-quiz-field='angle'>"
16400
+ + "<label>Angle<select class='studio-flat-select' data-quiz-field='angle'>"
16328
16401
  + QUIZ_ANGLES.map((candidate) => renderQuizOption(candidate, angle, getQuizAngleLabel(candidate))).join("")
16329
16402
  + "</select></label>"
16330
- + "<label>Thinking<select data-quiz-field='thinking'>"
16403
+ + "<label>Thinking<select class='studio-flat-select' data-quiz-field='thinking'>"
16331
16404
  + QUIZ_THINKING_LEVELS.map((candidate) => renderQuizOption(candidate, thinking, getQuizThinkingLabel(candidate))).join("")
16332
16405
  + "</select></label>"
16333
16406
  + "<label>Questions<input data-quiz-field='count' type='number' min='1' max='8' value='" + String(count) + "'></label>"
@@ -22490,6 +22563,7 @@
22490
22563
  }
22491
22564
 
22492
22565
  if (message.type === "side_question_state") {
22566
+ if (Array.isArray(message.sideQuestionThinkingLevels)) applySideQuestionThinkingLevels(message.sideQuestionThinkingLevels);
22493
22567
  sideQuestionWebSearchAvailable = message.webSearchAvailable === true;
22494
22568
  if (Array.isArray(message.availablePiTools)) applySideQuestionToolCatalog(message.availablePiTools);
22495
22569
  const previousStatus = sideQuestionState && sideQuestionState.status;
package/client/studio.css CHANGED
@@ -222,9 +222,30 @@
222
222
  }
223
223
 
224
224
  button {
225
+ -webkit-appearance: none;
226
+ appearance: none;
225
227
  cursor: pointer;
226
228
  }
227
229
 
230
+ .studio-flat-select {
231
+ -webkit-appearance: none;
232
+ appearance: none;
233
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 10 6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23717a84' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") !important;
234
+ background-position: right 8px center !important;
235
+ background-repeat: no-repeat !important;
236
+ background-size: 10px 6px !important;
237
+ padding-right: 26px !important;
238
+ }
239
+
240
+ @media (forced-colors: active) {
241
+ .studio-flat-select {
242
+ -webkit-appearance: auto;
243
+ appearance: auto;
244
+ background-image: none !important;
245
+ padding-right: 10px !important;
246
+ }
247
+ }
248
+
228
249
  button:not(:disabled):hover,
229
250
  select:hover {
230
251
  background: var(--panel-2);
@@ -442,20 +463,52 @@
442
463
  color: var(--accent);
443
464
  }
444
465
 
466
+ .studio-header-select-wrap {
467
+ position: relative;
468
+ display: inline-flex;
469
+ align-items: center;
470
+ min-width: 0;
471
+ }
472
+
473
+ .studio-header-select-wrap::after {
474
+ content: "⌄";
475
+ position: absolute;
476
+ top: 50%;
477
+ right: 4px;
478
+ color: var(--studio-info-text, var(--muted));
479
+ font-size: 14px;
480
+ font-weight: 600;
481
+ line-height: 1;
482
+ pointer-events: none;
483
+ transform: translateY(-58%);
484
+ }
485
+
486
+ .studio-header-select-wrap:hover::after,
487
+ .studio-header-select-wrap:focus-within::after {
488
+ color: var(--text);
489
+ }
490
+
445
491
  .section-header select {
446
492
  font-weight: 600;
447
493
  font-size: 14px;
448
494
  border: none;
449
495
  border-radius: 4px;
450
496
  background: inherit;
497
+ background-image: none;
451
498
  color: inherit;
452
499
  cursor: pointer;
453
- padding: 2px 4px;
500
+ padding: 2px 18px 2px 4px;
454
501
  margin: 0;
455
- -webkit-appearance: menulist;
456
- appearance: menulist;
502
+ -webkit-appearance: none;
503
+ appearance: none;
504
+ }
505
+
506
+ .section-header select:hover,
507
+ .section-header select:focus {
508
+ background-color: var(--studio-header-action-hover-bg, var(--panel-2));
457
509
  }
458
510
 
511
+ body[data-studio-mode="editor-only"] #editorViewSelectWrap,
459
512
  body[data-studio-mode="editor-only"] #editorViewSelect,
460
513
  body[data-studio-mode="editor-only"] #sendRunBtn,
461
514
  body[data-studio-mode="editor-only"] #queueSteerBtn,
@@ -464,7 +517,9 @@
464
517
  body[data-studio-mode="editor-only"] #critiqueBtn,
465
518
  body[data-studio-mode="editor-only"] #showMeBtn,
466
519
  body[data-studio-mode="editor-only"] #showMeResponseBtn,
520
+ body[data-studio-mode="editor-only"] #followSelectWrap,
467
521
  body[data-studio-mode="editor-only"] #followSelect,
522
+ body[data-studio-mode="editor-only"] #responseHighlightSelectWrap,
468
523
  body[data-studio-mode="editor-only"] #responseHighlightSelect,
469
524
  body[data-studio-mode="editor-only"] #pullLatestBtn,
470
525
  body[data-studio-mode="editor-only"] #historyPrevBtn,
@@ -496,6 +551,10 @@
496
551
  font-size: 14px;
497
552
  }
498
553
 
554
+ body[data-watched-file-preview="1"] #leftSectionHeader .section-header-main::before {
555
+ content: "Source (read-only)";
556
+ }
557
+
499
558
  body[data-watched-file-preview="1"] #rightSectionHeader .section-header-main::before {
500
559
  content: "Watched preview";
501
560
  }
@@ -3252,6 +3311,20 @@
3252
3311
  flex: 0 0 auto;
3253
3312
  }
3254
3313
 
3314
+ #responseActions .response-option-select-wrap {
3315
+ width: auto;
3316
+ flex: 0 0 auto;
3317
+ }
3318
+
3319
+ #responseActions .response-option-select-wrap select {
3320
+ width: 100%;
3321
+ min-width: 0;
3322
+ padding-right: 26px;
3323
+ -webkit-appearance: none;
3324
+ appearance: none;
3325
+ background-image: none;
3326
+ }
3327
+
3255
3328
  #critiqueView.git-changes-host {
3256
3329
  flex: 1 1 auto;
3257
3330
  min-height: 0;
@@ -4911,15 +4984,43 @@
4911
4984
  line-height: 1.35;
4912
4985
  }
4913
4986
 
4987
+ .studio-menu-select-wrap {
4988
+ position: relative;
4989
+ display: flex;
4990
+ width: 100%;
4991
+ min-width: 0;
4992
+ }
4993
+
4994
+ .studio-menu-select-wrap::after {
4995
+ content: "⌄";
4996
+ position: absolute;
4997
+ top: 50%;
4998
+ right: 8px;
4999
+ color: var(--studio-info-text, var(--muted));
5000
+ font-size: 14px;
5001
+ font-weight: 600;
5002
+ line-height: 1;
5003
+ pointer-events: none;
5004
+ transform: translateY(-58%);
5005
+ }
5006
+
5007
+ .studio-menu-select-wrap:hover::after,
5008
+ .studio-menu-select-wrap:focus-within::after {
5009
+ color: var(--text);
5010
+ }
5011
+
4914
5012
  .footer-model-menu-field select {
4915
5013
  width: 100%;
4916
5014
  min-width: 0;
4917
5015
  background: var(--panel-2);
5016
+ background-image: none;
4918
5017
  color: var(--text);
4919
5018
  border: 1px solid var(--control-border);
4920
5019
  border-radius: 8px;
4921
- padding: 5px 7px;
5020
+ padding: 5px 24px 5px 7px;
4922
5021
  font: inherit;
5022
+ -webkit-appearance: none;
5023
+ appearance: none;
4923
5024
  }
4924
5025
 
4925
5026
  .footer-model-menu-note {
@@ -5667,6 +5768,10 @@
5667
5768
  font-size: 12px;
5668
5769
  }
5669
5770
 
5771
+ body.studio-ui-refresh #responseActions .response-option-select-wrap select {
5772
+ padding-right: 24px;
5773
+ }
5774
+
5670
5775
 
5671
5776
  body.studio-ui-refresh main {
5672
5777
  gap: 0;
@@ -5832,8 +5937,11 @@
5832
5937
  body.studio-ui-refresh #rightSectionHeader #rightViewSelect {
5833
5938
  font-size: 14px;
5834
5939
  font-weight: 700;
5835
- padding: 3px 5px;
5940
+ padding: 3px 20px 3px 5px;
5836
5941
  max-width: 230px;
5942
+ -webkit-appearance: none;
5943
+ appearance: none;
5944
+ background-image: none;
5837
5945
  }
5838
5946
 
5839
5947
  body.studio-ui-refresh .studio-refresh-static-title {
@@ -5845,6 +5953,34 @@
5845
5953
  white-space: nowrap;
5846
5954
  }
5847
5955
 
5956
+ body.studio-ui-refresh .studio-watched-source-title {
5957
+ padding-right: 1px;
5958
+ }
5959
+
5960
+ body.studio-ui-refresh .studio-watched-source-read-only-badge {
5961
+ display: inline-flex;
5962
+ align-items: center;
5963
+ gap: 5px;
5964
+ min-height: 20px;
5965
+ padding: 1px 4px;
5966
+ border: 0;
5967
+ background: transparent;
5968
+ color: var(--studio-info-text, var(--muted));
5969
+ font-size: 11px;
5970
+ font-weight: 500;
5971
+ line-height: 1.2;
5972
+ white-space: nowrap;
5973
+ }
5974
+
5975
+ body.studio-ui-refresh .studio-watched-source-read-only-badge::before {
5976
+ content: "";
5977
+ width: 5px;
5978
+ height: 5px;
5979
+ border-radius: 999px;
5980
+ background: currentColor;
5981
+ opacity: 0.72;
5982
+ }
5983
+
5848
5984
  body.studio-ui-refresh #leftFocusBtn,
5849
5985
  body.studio-ui-refresh #rightFocusBtn {
5850
5986
  width: 29px;
package/index.ts CHANGED
@@ -74,9 +74,11 @@ import {
74
74
  import {
75
75
  buildStudioSideQuestionFollowUpPrompt,
76
76
  buildStudioSideQuestionPrompt,
77
+ getStudioSideQuestionThinkingLevels,
77
78
  normalizeStudioSideQuestionFocusKind,
78
79
  normalizeStudioSideQuestionGatherScope,
79
80
  normalizeStudioSideQuestionThinking,
81
+ resolveStudioSideQuestionThinking,
80
82
  STUDIO_SIDE_QUESTION_FOCUS_MAX_CHARS,
81
83
  STUDIO_SIDE_QUESTION_QUESTION_MAX_CHARS,
82
84
  } from "./shared/studio-side-question.js";
@@ -120,7 +122,7 @@ type StudioQuartoPreviewStatus = "idle" | "starting" | "running" | "stopping" |
120
122
  type StudioShowMeSourceKind = "selection" | "response" | "editor" | "context";
121
123
  type StudioSideQuestionFocusKind = "selection" | "section" | "editor" | "response" | "none";
122
124
  type StudioSideQuestionGatherScope = "none" | "folder" | "repo" | "custom";
123
- type StudioSideQuestionThinking = "off" | "minimal" | "low" | "medium" | "high";
125
+ type StudioSideQuestionThinking = ModelThinkingLevel;
124
126
  type StudioSideQuestionStatus = "idle" | "running" | "error";
125
127
 
126
128
  interface StudioQuartoPreviewContext {
@@ -8105,7 +8107,10 @@ async function respondLocalPreviewLinkJson(
8105
8107
  ...basePayload,
8106
8108
  converted,
8107
8109
  resourceDir: documentResourceDir,
8108
- relativeUrl: buildStudioRelativeUrl(serverState.token, "editor-only", document, docId, { skipWorkspaceRestore: true }),
8110
+ relativeUrl: buildStudioRelativeUrl(serverState.token, "editor-only", document, docId, {
8111
+ skipWorkspaceRestore: true,
8112
+ paneFocus: action === "watch-url" ? "right" : undefined,
8113
+ }),
8109
8114
  });
8110
8115
  }
8111
8116
 
@@ -11903,10 +11908,12 @@ ${cssVarsBlock}
11903
11908
  <section id="leftPane">
11904
11909
  <div id="leftSectionHeader" class="section-header">
11905
11910
  <div class="section-header-main">
11906
- <select id="editorViewSelect" aria-label="Editor view mode" title="Editor view mode. Shortcut: F7 when the editor pane is active; F6 switches panes.">
11907
- <option value="markdown" selected>Editor (Raw)</option>
11908
- <option value="preview">Editor (Preview)</option>
11909
- </select>
11911
+ <span id="editorViewSelectWrap" class="studio-header-select-wrap">
11912
+ <select id="editorViewSelect" aria-label="Editor view mode" title="Editor view mode. Shortcut: F7 when the editor pane is active; F6 switches panes.">
11913
+ <option value="markdown" selected>Editor (Raw)</option>
11914
+ <option value="preview">Editor (Preview)</option>
11915
+ </select>
11916
+ </span>
11910
11917
  </div>
11911
11918
  <div class="section-header-actions">
11912
11919
  <button id="leftFocusBtn" class="pane-focus-btn" type="button" title="Show only the editor pane. Shortcut: F10 or Cmd/Ctrl+Esc.">Focus pane</button>
@@ -11934,7 +11941,7 @@ ${cssVarsBlock}
11934
11941
  </div>
11935
11942
  <div class="source-actions-row repl-action-line" hidden>
11936
11943
  <button id="sendReplBtn" type="button" hidden title="Send the current selection, or the full editor text, to the active REPL session shown in the right pane.">Send to REPL</button>
11937
- <select id="replSendModeSelect" hidden aria-label="REPL send mode" title="Choose how Send to REPL interprets the editor text.">
11944
+ <select id="replSendModeSelect" class="studio-flat-select" hidden aria-label="REPL send mode" title="Choose how Send to REPL interprets the editor text.">
11938
11945
  <option value="raw" selected>Send mode: Raw</option>
11939
11946
  <option value="literate">Send mode: Literate</option>
11940
11947
  </select>
@@ -11943,11 +11950,11 @@ ${cssVarsBlock}
11943
11950
  <button id="copyDraftBtn" type="button" title="Copy the current editor text to the clipboard.">Copy</button>
11944
11951
  <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>
11945
11952
  <button id="suggestCompletionOptionsBtn" type="button" hidden title="Suggestion context options">▾</button>
11946
- <select id="completionContextSelect" hidden aria-label="Suggestion context mode" title="Choose how much context Suggest includes.">
11953
+ <select id="completionContextSelect" class="studio-flat-select" hidden aria-label="Suggestion context mode" title="Choose how much context Suggest includes.">
11947
11954
  <option value="cursor" selected>Context: editor only</option>
11948
11955
  <option value="session">Context: editor + latest response</option>
11949
11956
  </select>
11950
- <select id="completionModelSelect" hidden aria-label="Suggestion model" title="Choose the model used for Suggest. Suggestions use direct completion with thinking off and do not change the main Pi model.">
11957
+ <select id="completionModelSelect" class="studio-flat-select" hidden aria-label="Suggestion model" title="Choose the model used for Suggest. Suggestions use direct completion with thinking off and do not change the main Pi model.">
11951
11958
  <option value="current" selected>Suggestion model: current Pi model</option>
11952
11959
  </select>
11953
11960
  <button id="openCompanionBtn" type="button" title="Open a blank editor-only Studio tab.">New editor tab</button>
@@ -11955,7 +11962,7 @@ ${cssVarsBlock}
11955
11962
  </div>
11956
11963
  <div class="source-actions-row">
11957
11964
  <button id="insertHeaderBtn" type="button" title="Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).">Annotation header</button>
11958
- <select id="annotationModeSelect" aria-label="Inline annotation visibility mode" title="On: keep and send [an: ...] markers. Hide: keep markers in the editor, hide them in preview, and strip before Run/Critique.">
11965
+ <select id="annotationModeSelect" class="studio-flat-select" aria-label="Inline annotation visibility mode" title="On: keep and send [an: ...] markers. Hide: keep markers in the editor, hide them in preview, and strip before Run/Critique.">
11959
11966
  <option value="on" selected>Inline annotations: On</option>
11960
11967
  <option value="off">Inline annotations: Hide</option>
11961
11968
  </select>
@@ -11963,7 +11970,7 @@ ${cssVarsBlock}
11963
11970
  <button id="saveAnnotatedBtn" type="button" title="Save full editor content (including [an: ...] markers) as a .annotated.md file.">Save .annotated.md</button>
11964
11971
  </div>
11965
11972
  <div class="source-actions-row">
11966
- <select id="lensSelect" aria-label="Critique focus">
11973
+ <select id="lensSelect" class="studio-flat-select" aria-label="Critique focus">
11967
11974
  <option value="auto" selected>Critique: Auto</option>
11968
11975
  <option value="writing">Critique: Writing</option>
11969
11976
  <option value="code">Critique: Code</option>
@@ -11973,7 +11980,7 @@ ${cssVarsBlock}
11973
11980
  <button id="showMeResponseBtn" type="button" hidden title="Explain the response displayed in the right pane using the smallest useful visual or structural representation.">Explain displayed response</button>
11974
11981
  <button id="askAsideBtn" type="button" title="Open a separate side-question thread without adding it to the main Pi conversation.">Side question</button>
11975
11982
  <button id="quizBtn" type="button" title="Open an active quiz for the current editor selection or document.">Quiz me</button>
11976
- <select id="highlightSelect" aria-label="Editor syntax highlighting">
11983
+ <select id="highlightSelect" class="studio-flat-select" aria-label="Editor syntax highlighting">
11977
11984
  <option value="off">Syntax highlight: Off</option>
11978
11985
  <option value="bash">Syntax highlight: Bash</option>
11979
11986
  <option value="c">Syntax highlight: C</option>
@@ -12003,11 +12010,11 @@ ${cssVarsBlock}
12003
12010
  <option value="xml">Syntax highlight: XML</option>
12004
12011
  <option value="yaml">Syntax highlight: YAML</option>
12005
12012
  </select>
12006
- <select id="lineNumbersSelect" aria-label="Editor line numbers">
12013
+ <select id="lineNumbersSelect" class="studio-flat-select" aria-label="Editor line numbers">
12007
12014
  <option value="off">Line numbers: Off</option>
12008
12015
  <option value="on" selected>Line numbers: On</option>
12009
12016
  </select>
12010
- <select id="editorFontSizeSelect" aria-label="Editor text size" title="Adjust raw editor text size.">
12017
+ <select id="editorFontSizeSelect" class="studio-flat-select" aria-label="Editor text size" title="Adjust raw editor text size.">
12011
12018
  <option value="10">Editor text: 10px</option>
12012
12019
  <option value="11">Editor text: 11px</option>
12013
12020
  <option value="12" selected>Editor text: 12px</option>
@@ -12105,17 +12112,19 @@ ${cssVarsBlock}
12105
12112
  <section id="rightPane">
12106
12113
  <div id="rightSectionHeader" class="section-header">
12107
12114
  <div class="section-header-main">
12108
- <select id="rightViewSelect" aria-label="Response view mode" title="Right pane view mode. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+1–8 switches directly between all right-pane views. Cmd/Ctrl+Alt+P/E/W/F/Q keep mnemonic shortcuts for Preview, Editor Preview, Working, Files, and Side questions.">
12109
- <option value="markdown">Response (Raw)</option>
12110
- <option value="preview" selected>Response (Preview)</option>
12111
- <option value="editor-preview">Editor (Preview)</option>
12112
- <option value="editor-quarto-preview" hidden>Editor (Quarto Preview)</option>
12113
- <option value="trace">Working</option>
12114
- <option value="changes">Changes</option>
12115
- <option value="files">Files</option>
12116
- <option value="repl">REPL</option>
12117
- <option value="side-questions">Side questions</option>
12118
- </select>
12115
+ <span id="rightViewSelectWrap" class="studio-header-select-wrap">
12116
+ <select id="rightViewSelect" aria-label="Response view mode" title="Right pane view mode. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+1–8 switches directly between all right-pane views. Cmd/Ctrl+Alt+P/E/W/F/Q keep mnemonic shortcuts for Preview, Editor Preview, Working, Files, and Side questions.">
12117
+ <option value="markdown">Response (Raw)</option>
12118
+ <option value="preview" selected>Response (Preview)</option>
12119
+ <option value="editor-preview">Editor (Preview)</option>
12120
+ <option value="editor-quarto-preview" hidden>Editor (Quarto Preview)</option>
12121
+ <option value="trace">Working</option>
12122
+ <option value="changes">Changes</option>
12123
+ <option value="files">Files</option>
12124
+ <option value="repl">REPL</option>
12125
+ <option value="side-questions">Side questions</option>
12126
+ </select>
12127
+ </span>
12119
12128
  </div>
12120
12129
  <div class="section-header-actions">
12121
12130
  <button id="rightFocusBtn" class="pane-focus-btn" type="button" title="Show only the response pane. Shortcut: F10 or Cmd/Ctrl+Esc.">Focus pane</button>
@@ -12142,28 +12151,34 @@ ${cssVarsBlock}
12142
12151
  <div class="response-wrap">
12143
12152
  <div id="responseActions" class="response-actions">
12144
12153
  <div class="response-actions-row response-options-row">
12145
- <select id="followSelect" aria-label="Auto-update response">
12146
- <option value="on" selected>Auto-update response: On</option>
12147
- <option value="off">Auto-update response: Off</option>
12148
- </select>
12149
- <select id="responseHighlightSelect" aria-label="Response markdown highlighting">
12150
- <option value="off">Syntax highlight: Off</option>
12151
- <option value="on" selected>Syntax highlight: On</option>
12152
- </select>
12153
- <select id="responseFontSizeSelect" aria-label="Response text size" title="Adjust right-pane response, preview, and working text size.">
12154
- <option value="11">Response text: 11px</option>
12155
- <option value="12">Response text: 12px</option>
12156
- <option value="12.5">Response text: 12.5px</option>
12157
- <option value="13">Response text: 13px</option>
12158
- <option value="13.5" selected>Response text: 13.5px</option>
12159
- <option value="14">Response text: 14px</option>
12160
- <option value="14.5">Response text: 14.5px</option>
12161
- <option value="15">Response text: 15px</option>
12162
- <option value="15.5">Response text: 15.5px</option>
12163
- <option value="16">Response text: 16px</option>
12164
- <option value="18">Response text: 18px</option>
12165
- <option value="20">Response text: 20px</option>
12166
- </select>
12154
+ <span id="followSelectWrap" class="studio-menu-select-wrap response-option-select-wrap">
12155
+ <select id="followSelect" aria-label="Auto-update response">
12156
+ <option value="on" selected>Auto-update response: On</option>
12157
+ <option value="off">Auto-update response: Off</option>
12158
+ </select>
12159
+ </span>
12160
+ <span id="responseHighlightSelectWrap" class="studio-menu-select-wrap response-option-select-wrap">
12161
+ <select id="responseHighlightSelect" aria-label="Response markdown highlighting">
12162
+ <option value="off">Syntax highlight: Off</option>
12163
+ <option value="on" selected>Syntax highlight: On</option>
12164
+ </select>
12165
+ </span>
12166
+ <span id="responseFontSizeSelectWrap" class="studio-menu-select-wrap response-option-select-wrap">
12167
+ <select id="responseFontSizeSelect" aria-label="Response text size" title="Adjust right-pane response, preview, and working text size.">
12168
+ <option value="11">Response text: 11px</option>
12169
+ <option value="12">Response text: 12px</option>
12170
+ <option value="12.5">Response text: 12.5px</option>
12171
+ <option value="13">Response text: 13px</option>
12172
+ <option value="13.5" selected>Response text: 13.5px</option>
12173
+ <option value="14">Response text: 14px</option>
12174
+ <option value="14.5">Response text: 14.5px</option>
12175
+ <option value="15">Response text: 15px</option>
12176
+ <option value="15.5">Response text: 15.5px</option>
12177
+ <option value="16">Response text: 16px</option>
12178
+ <option value="18">Response text: 18px</option>
12179
+ <option value="20">Response text: 20px</option>
12180
+ </select>
12181
+ </span>
12167
12182
  </div>
12168
12183
  <div class="response-actions-row history-row">
12169
12184
  <button id="pullLatestBtn" type="button" title="Fetch the latest assistant response when auto-update is off.">Fetch latest response</button>
@@ -13692,6 +13707,9 @@ export default function (pi: ExtensionAPI) {
13692
13707
  }
13693
13708
  : null;
13694
13709
 
13710
+ const getCurrentStudioSideQuestionThinkingLevels = (): StudioSideQuestionThinking[] =>
13711
+ getStudioSideQuestionThinkingLevels(latestModelRequestCtx?.model ?? lastCommandCtx?.model) as StudioSideQuestionThinking[];
13712
+
13695
13713
  const getStudioThemeOptions = () => {
13696
13714
  try {
13697
13715
  const themes = lastCommandCtx?.ui?.getAllThemes?.() ?? [];
@@ -13727,6 +13745,7 @@ export default function (pi: ExtensionAPI) {
13727
13745
  modelLabel: currentModelLabel,
13728
13746
  currentModel: getCurrentStudioModelDescriptor(),
13729
13747
  thinkingLevel: getThinkingLevelSafe() ?? "off",
13748
+ sideQuestionThinkingLevels: getCurrentStudioSideQuestionThinkingLevels(),
13730
13749
  piModels: modelOptions,
13731
13750
  suggestionModels: modelOptions,
13732
13751
  currentTheme: getCurrentStudioThemeDescriptor(),
@@ -14033,6 +14052,7 @@ export default function (pi: ExtensionAPI) {
14033
14052
  const payload: Record<string, unknown> = {
14034
14053
  type: "side_question_state",
14035
14054
  state: getStudioSideQuestionPublicState(),
14055
+ sideQuestionThinkingLevels: getCurrentStudioSideQuestionThinkingLevels(),
14036
14056
  webSearchAvailable: Boolean(String(process.env.BRAVE_API_KEY || "").trim()),
14037
14057
  };
14038
14058
  if (client) payload.availablePiTools = getPublicStudioSideQuestionToolCatalog();
@@ -14254,6 +14274,7 @@ export default function (pi: ExtensionAPI) {
14254
14274
  const model = latestModelRequestCtx?.model ?? ctx.model;
14255
14275
  if (!model) throw new Error("No active Pi model is available for side questions.");
14256
14276
  await resolveStudioModelRequestAuth({ model, modelRegistry: ctx.modelRegistry }, model);
14277
+ const thinking = resolveStudioSideQuestionThinking(model, context.thinking) as StudioSideQuestionThinking;
14257
14278
  const gitSnapshot = context.gitContext
14258
14279
  ? await captureStudioSideQuestionGitSnapshot(contextRoot, { runGit: runStudioSideQuestionGitCommand }) as StudioSideQuestionGitSnapshot
14259
14280
  : null;
@@ -14292,7 +14313,7 @@ export default function (pi: ExtensionAPI) {
14292
14313
  customTools: tools,
14293
14314
  sessionManager: sideSessionManager,
14294
14315
  model,
14295
- thinking: context.thinking,
14316
+ thinking,
14296
14317
  inheritedPrompt: stripStudioDynamicSystemPromptFooter(ctx.getSystemPrompt()),
14297
14318
  });
14298
14319
  session = agentRuntime.session;
@@ -14301,7 +14322,7 @@ export default function (pi: ExtensionAPI) {
14301
14322
  cwd: workingDirectory,
14302
14323
  sessionManager: sideSessionManager,
14303
14324
  model,
14304
- thinkingLevel: context.thinking,
14325
+ thinkingLevel: thinking,
14305
14326
  tools: activeToolNames,
14306
14327
  customTools: tools,
14307
14328
  resourceLoader: createStudioSideQuestionResourceLoader(ctx),
@@ -14334,7 +14355,7 @@ export default function (pi: ExtensionAPI) {
14334
14355
  tools: selectedPiTools,
14335
14356
  },
14336
14357
  modelLabel: formatStudioModelOptionLabel(model),
14337
- thinking: context.thinking,
14358
+ thinking,
14338
14359
  messages: [],
14339
14360
  activity: [],
14340
14361
  error: context.webSearch && !webAvailable ? "Web search was requested but BRAVE_API_KEY is not configured; this thread will use local context only." : "",
@@ -14638,6 +14659,7 @@ export default function (pi: ExtensionAPI) {
14638
14659
  modelLabel: currentModelLabel,
14639
14660
  currentModel: getCurrentStudioModelDescriptor(),
14640
14661
  thinkingLevel: getThinkingLevelSafe() ?? "off",
14662
+ sideQuestionThinkingLevels: getCurrentStudioSideQuestionThinkingLevels(),
14641
14663
  piModels: getStudioModelOptions(),
14642
14664
  suggestionModels: getStudioModelOptions(),
14643
14665
  currentTheme: getCurrentStudioThemeDescriptor(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.9.54",
3
+ "version": "0.9.55",
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",
@@ -1,5 +1,16 @@
1
+ import { clampThinkingLevel, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
2
+
1
3
  export const STUDIO_SIDE_QUESTION_FOCUS_MAX_CHARS = 60_000;
2
4
  export const STUDIO_SIDE_QUESTION_QUESTION_MAX_CHARS = 12_000;
5
+ export const STUDIO_SIDE_QUESTION_THINKING_LEVELS = Object.freeze([
6
+ "off",
7
+ "minimal",
8
+ "low",
9
+ "medium",
10
+ "high",
11
+ "xhigh",
12
+ "max",
13
+ ]);
3
14
 
4
15
  export function normalizeStudioSideQuestionFocusKind(value) {
5
16
  const normalized = String(value ?? "").trim().toLowerCase();
@@ -20,10 +31,28 @@ export function normalizeStudioSideQuestionGatherScope(value) {
20
31
 
21
32
  export function normalizeStudioSideQuestionThinking(value) {
22
33
  const normalized = String(value ?? "").trim().toLowerCase();
23
- if (normalized === "off" || normalized === "minimal" || normalized === "low" || normalized === "medium" || normalized === "high") {
24
- return normalized;
34
+ return STUDIO_SIDE_QUESTION_THINKING_LEVELS.includes(normalized) ? normalized : "low";
35
+ }
36
+
37
+ export function getStudioSideQuestionThinkingLevels(model) {
38
+ if (!model) return [...STUDIO_SIDE_QUESTION_THINKING_LEVELS];
39
+ try {
40
+ const supported = getSupportedThinkingLevels(model)
41
+ .filter((level) => STUDIO_SIDE_QUESTION_THINKING_LEVELS.includes(level));
42
+ return supported.length > 0 ? supported : ["off"];
43
+ } catch {
44
+ return [...STUDIO_SIDE_QUESTION_THINKING_LEVELS];
45
+ }
46
+ }
47
+
48
+ export function resolveStudioSideQuestionThinking(model, value) {
49
+ const requested = normalizeStudioSideQuestionThinking(value);
50
+ if (!model) return requested;
51
+ try {
52
+ return clampThinkingLevel(model, requested);
53
+ } catch {
54
+ return requested;
25
55
  }
26
- return "low";
27
56
  }
28
57
 
29
58
  function sanitizePromptContent(value) {