pi-studio 0.9.27 → 0.9.28

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.28] — 2026-06-08
8
+
9
+ ### Added
10
+ - Added a footer **Pi model & thinking** menu for switching the active Pi model and thinking level from Studio while keeping Studio Suggest model choice separate.
11
+
12
+ ### Changed
13
+ - Clarified the browser-import tooltip to explain that **Save editor as…** can make an imported copy file-backed.
14
+ - Regularized Source & context menu notes so explanatory text uses a quieter, consistent menu-note style.
15
+
7
16
  ## [0.9.27] — 2026-06-08
8
17
 
9
18
  ### Added
@@ -7,6 +7,7 @@
7
7
  const footerMetaModelEl = document.getElementById("footerMetaModel");
8
8
  const footerMetaTerminalEl = document.getElementById("footerMetaTerminal");
9
9
  const footerMetaContextEl = document.getElementById("footerMetaContext");
10
+ const footerModelMenuEl = document.getElementById("footerModelMenu");
10
11
  let faviconLinkEl = document.querySelector('link[rel="icon"], link[rel="shortcut icon"]');
11
12
  if (!faviconLinkEl) {
12
13
  faviconLinkEl = document.createElement("link");
@@ -522,6 +523,10 @@
522
523
  let previewExportInProgress = false;
523
524
  let compactInProgress = false;
524
525
  let modelLabel = (document.body && document.body.dataset && document.body.dataset.modelLabel) || "none";
526
+ let piModelOptions = [];
527
+ let piCurrentModel = null;
528
+ let piThinkingLevel = "";
529
+ let footerModelMenuOpen = false;
525
530
  let terminalSessionLabel = (document.body && document.body.dataset && document.body.dataset.terminalLabel) || "unknown";
526
531
  let terminalSessionDetail = (document.body && document.body.dataset && document.body.dataset.terminalDetail) || terminalSessionLabel;
527
532
  let contextTokens = null;
@@ -2510,13 +2515,13 @@
2510
2515
  completionModelSelect.hidden = false;
2511
2516
  suggestionItems.push(completionModelSelect);
2512
2517
  }
2513
- const completionThinkingNoteEl = makeStudioUiRefreshElement("div", "source-badge completion-thinking-note", "Suggest uses thinking off and does not change the main Pi model.");
2518
+ const completionThinkingNoteEl = makeStudioUiRefreshElement("div", "studio-refresh-menu-note completion-thinking-note", "Suggest: thinking off; model choice only affects suggestions.");
2514
2519
  completionThinkingNoteEl.setAttribute("aria-label", "Suggestion model note");
2515
2520
  suggestionItems.push(completionThinkingNoteEl);
2516
2521
  appendStudioUiRefreshMenuSection(contextMenu.menu, "Suggestions", suggestionItems);
2517
2522
  const statusItems = [];
2518
2523
  if (!isEditorOnlyMode) {
2519
- sourceSessionSummaryEl = makeStudioUiRefreshElement("div", "source-badge source-session-summary", "Session tree: branch history follows the current Pi branch. Editor text is independent.");
2524
+ sourceSessionSummaryEl = makeStudioUiRefreshElement("div", "studio-refresh-menu-note source-session-summary", "Session tree: branch history follows the current Pi branch; editor text stays independent.");
2520
2525
  sourceSessionSummaryEl.setAttribute("aria-label", "Pi session tree and editor sync behaviour");
2521
2526
  sourceSessionSummaryEl.title = "Use /tree in the Pi terminal to navigate branches. Studio updates branch history to match the active branch and leaves editor text unchanged.";
2522
2527
  statusItems.push(sourceSessionSummaryEl);
@@ -3179,6 +3184,118 @@
3179
3184
  }
3180
3185
  }
3181
3186
 
3187
+ function encodePiModelValue(provider, id) {
3188
+ return JSON.stringify([String(provider || ""), String(id || "")]);
3189
+ }
3190
+
3191
+ function decodePiModelValue(value) {
3192
+ try {
3193
+ const parsed = JSON.parse(String(value || ""));
3194
+ if (!Array.isArray(parsed) || parsed.length < 2) return null;
3195
+ const provider = String(parsed[0] || "").trim();
3196
+ const id = String(parsed[1] || "").trim();
3197
+ return provider && id ? { provider, id } : null;
3198
+ } catch {
3199
+ return null;
3200
+ }
3201
+ }
3202
+
3203
+ function normalizePiModelOptions(options) {
3204
+ return Array.isArray(options)
3205
+ ? options.map((option) => ({
3206
+ provider: String(option && option.provider || "").trim(),
3207
+ id: String(option && option.id || "").trim(),
3208
+ label: String(option && option.label || "").trim(),
3209
+ reasoning: Boolean(option && option.reasoning),
3210
+ })).filter((option) => option.provider && option.id)
3211
+ : [];
3212
+ }
3213
+
3214
+ function updatePiSessionModelState(message) {
3215
+ if (!message || typeof message !== "object") return;
3216
+ if (Array.isArray(message.piModels)) {
3217
+ piModelOptions = normalizePiModelOptions(message.piModels);
3218
+ } else if (Array.isArray(message.suggestionModels) && !piModelOptions.length) {
3219
+ piModelOptions = normalizePiModelOptions(message.suggestionModels);
3220
+ }
3221
+ if (message.currentModel && typeof message.currentModel === "object") {
3222
+ const model = message.currentModel;
3223
+ const provider = String(model.provider || "").trim();
3224
+ const id = String(model.id || "").trim();
3225
+ piCurrentModel = provider && id ? {
3226
+ provider,
3227
+ id,
3228
+ label: String(model.label || "").trim(),
3229
+ reasoning: Boolean(model.reasoning),
3230
+ } : null;
3231
+ }
3232
+ if (typeof message.thinkingLevel === "string") {
3233
+ piThinkingLevel = message.thinkingLevel.trim();
3234
+ }
3235
+ renderFooterModelMenu();
3236
+ }
3237
+
3238
+ function getPiCurrentModelValue() {
3239
+ return piCurrentModel && piCurrentModel.provider && piCurrentModel.id
3240
+ ? encodePiModelValue(piCurrentModel.provider, piCurrentModel.id)
3241
+ : "";
3242
+ }
3243
+
3244
+ function getPiThinkingLevels() {
3245
+ return ["off", "minimal", "low", "medium", "high", "xhigh"];
3246
+ }
3247
+
3248
+ function renderFooterModelMenu() {
3249
+ if (!footerModelMenuEl) return;
3250
+ const currentValue = getPiCurrentModelValue();
3251
+ const optionValues = new Set(piModelOptions.map((option) => encodePiModelValue(option.provider, option.id)));
3252
+ const modelOptionsHtml = piModelOptions.map((option) => {
3253
+ const value = encodePiModelValue(option.provider, option.id);
3254
+ const label = option.label || (option.provider + "/" + option.id);
3255
+ return "<option value='" + escapeHtml(value) + "'" + (value === currentValue ? " selected" : "") + ">" + escapeHtml(label) + "</option>";
3256
+ });
3257
+ if (currentValue && !optionValues.has(currentValue)) {
3258
+ const label = piCurrentModel && piCurrentModel.label ? piCurrentModel.label : modelLabel;
3259
+ modelOptionsHtml.unshift("<option value='" + escapeHtml(currentValue) + "' selected>" + escapeHtml(label || "current model") + "</option>");
3260
+ }
3261
+ const thinking = piThinkingLevel || "off";
3262
+ const thinkingOptionsHtml = getPiThinkingLevels().map((level) => {
3263
+ return "<option value='" + escapeHtml(level) + "'" + (level === thinking ? " selected" : "") + ">Thinking: " + escapeHtml(level) + "</option>";
3264
+ });
3265
+ footerModelMenuEl.innerHTML = ""
3266
+ + "<div class='footer-model-menu-heading'>Pi model & thinking</div>"
3267
+ + "<label class='footer-model-menu-field'><span>Pi model</span><select id='footerPiModelSelect'>" + modelOptionsHtml.join("") + "</select></label>"
3268
+ + "<label class='footer-model-menu-field'><span>Thinking</span><select id='footerPiThinkingSelect'>" + thinkingOptionsHtml.join("") + "</select></label>"
3269
+ + "<div class='footer-model-menu-note'>Affects future Pi turns. Studio Suggest has its own model setting.</div>";
3270
+ }
3271
+
3272
+ function setFooterModelMenuOpen(open) {
3273
+ footerModelMenuOpen = Boolean(open);
3274
+ if (footerModelMenuEl) footerModelMenuEl.hidden = !footerModelMenuOpen;
3275
+ if (footerMetaModelEl) {
3276
+ footerMetaModelEl.classList.toggle("is-open", footerModelMenuOpen);
3277
+ footerMetaModelEl.setAttribute("aria-expanded", footerModelMenuOpen ? "true" : "false");
3278
+ }
3279
+ if (footerModelMenuOpen) renderFooterModelMenu();
3280
+ }
3281
+
3282
+ function requestPiModelSelection(value) {
3283
+ const model = decodePiModelValue(value);
3284
+ if (!model) {
3285
+ setStatus("No Pi model selected.", "warning");
3286
+ return;
3287
+ }
3288
+ const sent = sendMessage({ type: "pi_model_select_request", provider: model.provider, id: model.id });
3289
+ if (sent) setStatus("Switching Pi model…", "warning");
3290
+ }
3291
+
3292
+ function requestPiThinkingLevel(level) {
3293
+ const normalized = String(level || "").trim();
3294
+ if (!normalized) return;
3295
+ const sent = sendMessage({ type: "pi_thinking_level_request", level: normalized });
3296
+ if (sent) setStatus("Setting Pi thinking level…", "warning");
3297
+ }
3298
+
3182
3299
  function updateFooterMeta() {
3183
3300
  const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
3184
3301
  const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
@@ -3192,7 +3309,9 @@
3192
3309
  footerMetaModelEl.textContent = modelText;
3193
3310
  footerMetaTerminalEl.textContent = terminalText;
3194
3311
  footerMetaContextEl.textContent = contextDisplayText;
3195
- footerMetaModelEl.title = "Model: " + modelText;
3312
+ footerMetaModelEl.title = "Pi model and thinking: " + modelText;
3313
+ footerMetaModelEl.setAttribute("aria-haspopup", "menu");
3314
+ footerMetaModelEl.setAttribute("aria-expanded", footerModelMenuOpen ? "true" : "false");
3196
3315
  footerMetaTerminalEl.title = terminalDetailText;
3197
3316
  footerMetaContextEl.title = contextTitleText;
3198
3317
  if (footerMetaTextEl) footerMetaTextEl.title = titleText;
@@ -18649,6 +18768,7 @@
18649
18768
  if (Array.isArray(message.suggestionModels)) {
18650
18769
  updateCompletionSuggestionModelOptions(message.suggestionModels);
18651
18770
  }
18771
+ updatePiSessionModelState(message);
18652
18772
  if (typeof message.terminalSessionLabel === "string") {
18653
18773
  terminalSessionLabel = message.terminalSessionLabel;
18654
18774
  }
@@ -19224,6 +19344,7 @@
19224
19344
  if (Array.isArray(message.suggestionModels)) {
19225
19345
  updateCompletionSuggestionModelOptions(message.suggestionModels);
19226
19346
  }
19347
+ updatePiSessionModelState(message);
19227
19348
  if (typeof message.terminalSessionLabel === "string") {
19228
19349
  terminalSessionLabel = message.terminalSessionLabel;
19229
19350
  }
@@ -20103,9 +20224,39 @@
20103
20224
  if (event.key === "Escape") {
20104
20225
  closeExportPreviewMenu();
20105
20226
  closePreviewLinkMenu();
20227
+ setFooterModelMenuOpen(false);
20106
20228
  }
20107
20229
  });
20108
20230
 
20231
+ if (footerMetaModelEl) {
20232
+ footerMetaModelEl.addEventListener("click", (event) => {
20233
+ event.preventDefault();
20234
+ event.stopPropagation();
20235
+ setFooterModelMenuOpen(!footerModelMenuOpen);
20236
+ });
20237
+ }
20238
+ if (footerModelMenuEl) {
20239
+ footerModelMenuEl.addEventListener("click", (event) => {
20240
+ event.stopPropagation();
20241
+ });
20242
+ footerModelMenuEl.addEventListener("change", (event) => {
20243
+ const target = event.target;
20244
+ if (!(target instanceof HTMLSelectElement)) return;
20245
+ if (target.id === "footerPiModelSelect") {
20246
+ requestPiModelSelection(target.value);
20247
+ setFooterModelMenuOpen(false);
20248
+ } else if (target.id === "footerPiThinkingSelect") {
20249
+ requestPiThinkingLevel(target.value);
20250
+ setFooterModelMenuOpen(false);
20251
+ }
20252
+ });
20253
+ }
20254
+ document.addEventListener("click", (event) => {
20255
+ const target = event.target;
20256
+ if (target instanceof Element && (target.closest("#footerModelMenu") || target.closest("#footerMetaModel"))) return;
20257
+ setFooterModelMenuOpen(false);
20258
+ });
20259
+
20109
20260
  saveAsBtn.addEventListener("click", () => {
20110
20261
  const content = sourceTextEl.value;
20111
20262
  if (!content.trim()) {
package/client/studio.css CHANGED
@@ -4241,6 +4241,82 @@
4241
4241
  max-width: 34ch;
4242
4242
  }
4243
4243
 
4244
+ .footer-model-btn {
4245
+ border: 1px solid transparent;
4246
+ border-radius: 7px;
4247
+ background: transparent;
4248
+ color: inherit;
4249
+ font: inherit;
4250
+ text-align: left;
4251
+ padding: 2px 4px;
4252
+ cursor: pointer;
4253
+ }
4254
+
4255
+ .footer-model-btn:hover,
4256
+ .footer-model-btn:focus-visible,
4257
+ .footer-model-btn.is-open {
4258
+ background: var(--panel-2);
4259
+ border-color: var(--control-border);
4260
+ color: var(--text);
4261
+ outline: none;
4262
+ }
4263
+
4264
+ .footer-model-menu {
4265
+ position: fixed;
4266
+ left: 12px;
4267
+ bottom: 38px;
4268
+ width: min(440px, calc(100vw - 24px));
4269
+ padding: 10px;
4270
+ border: 1px solid var(--panel-border);
4271
+ border-radius: 12px;
4272
+ background: var(--panel);
4273
+ color: var(--text);
4274
+ box-shadow: 0 18px 46px var(--shadow-color);
4275
+ z-index: 110;
4276
+ }
4277
+
4278
+ .footer-model-menu[hidden] {
4279
+ display: none !important;
4280
+ }
4281
+
4282
+ .footer-model-menu-heading {
4283
+ margin: 0 2px 8px;
4284
+ color: var(--muted);
4285
+ font-size: 11px;
4286
+ font-weight: 650;
4287
+ text-transform: uppercase;
4288
+ letter-spacing: 0.06em;
4289
+ }
4290
+
4291
+ .footer-model-menu-field {
4292
+ display: grid;
4293
+ gap: 4px;
4294
+ margin: 8px 0;
4295
+ min-width: 0;
4296
+ }
4297
+
4298
+ .footer-model-menu-field span,
4299
+ .footer-model-menu-note {
4300
+ color: var(--muted);
4301
+ font-size: 12px;
4302
+ line-height: 1.35;
4303
+ }
4304
+
4305
+ .footer-model-menu-field select {
4306
+ width: 100%;
4307
+ min-width: 0;
4308
+ background: var(--panel-2);
4309
+ color: var(--text);
4310
+ border: 1px solid var(--control-border);
4311
+ border-radius: 8px;
4312
+ padding: 5px 7px;
4313
+ font: inherit;
4314
+ }
4315
+
4316
+ .footer-model-menu-note {
4317
+ margin-top: 8px;
4318
+ }
4319
+
4244
4320
  .footer-meta-terminal {
4245
4321
  flex: 0 4 auto;
4246
4322
  max-width: 34ch;
@@ -5629,9 +5705,7 @@
5629
5705
  font-weight: 450;
5630
5706
  }
5631
5707
 
5632
- body.studio-ui-refresh .studio-refresh-menu-item > .source-origin-summary,
5633
- body.studio-ui-refresh .studio-refresh-menu-item > .source-session-summary,
5634
- body.studio-ui-refresh .studio-refresh-menu-item > .completion-thinking-note {
5708
+ body.studio-ui-refresh .studio-refresh-menu-item > .source-origin-summary {
5635
5709
  width: 100%;
5636
5710
  min-width: 0;
5637
5711
  border-color: var(--border-subtle);
@@ -5640,6 +5714,18 @@
5640
5714
  white-space: normal;
5641
5715
  overflow-wrap: anywhere;
5642
5716
  line-height: 1.35;
5717
+ font-weight: 500;
5718
+ }
5719
+
5720
+ body.studio-ui-refresh .studio-refresh-menu-note {
5721
+ width: 100%;
5722
+ min-width: 0;
5723
+ padding: 4px 6px;
5724
+ color: var(--muted);
5725
+ font-size: 12px;
5726
+ line-height: 1.35;
5727
+ white-space: normal;
5728
+ overflow-wrap: anywhere;
5643
5729
  }
5644
5730
 
5645
5731
  body.studio-ui-refresh .studio-refresh-menu #critiqueBtn {
package/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext, SessionEntry, Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
3
- import { completeSimple, type ThinkingLevel } from "@earendil-works/pi-ai";
3
+ import { completeSimple, type ModelThinkingLevel, type ThinkingLevel } from "@earendil-works/pi-ai";
4
4
  import { Type } from "@sinclair/typebox";
5
5
  import { spawn, spawnSync } from "node:child_process";
6
6
  import { createHash, randomUUID } from "node:crypto";
@@ -349,6 +349,17 @@ interface CompletionSuggestionCancelRequestMessage {
349
349
  requestId: string;
350
350
  }
351
351
 
352
+ interface PiModelSelectRequestMessage {
353
+ type: "pi_model_select_request";
354
+ provider: string;
355
+ id: string;
356
+ }
357
+
358
+ interface PiThinkingLevelRequestMessage {
359
+ type: "pi_thinking_level_request";
360
+ level: ModelThinkingLevel;
361
+ }
362
+
352
363
  interface QuizGenerateRequestMessage {
353
364
  type: "quiz_generate_request";
354
365
  requestId: string;
@@ -492,6 +503,8 @@ type IncomingStudioMessage =
492
503
  | SendRunRequestMessage
493
504
  | CompletionSuggestionRequestMessage
494
505
  | CompletionSuggestionCancelRequestMessage
506
+ | PiModelSelectRequestMessage
507
+ | PiThinkingLevelRequestMessage
495
508
  | QuizGenerateRequestMessage
496
509
  | QuizAnswerRequestMessage
497
510
  | QuizDiscussRequestMessage
@@ -8324,6 +8337,24 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
8324
8337
  };
8325
8338
  }
8326
8339
 
8340
+ if (msg.type === "pi_model_select_request" && typeof msg.provider === "string" && typeof msg.id === "string") {
8341
+ return {
8342
+ type: "pi_model_select_request",
8343
+ provider: msg.provider,
8344
+ id: msg.id,
8345
+ };
8346
+ }
8347
+
8348
+ if (msg.type === "pi_thinking_level_request" && typeof msg.level === "string") {
8349
+ const level = msg.level.trim().toLowerCase();
8350
+ if (level === "off" || level === "minimal" || level === "low" || level === "medium" || level === "high" || level === "xhigh") {
8351
+ return {
8352
+ type: "pi_thinking_level_request",
8353
+ level,
8354
+ };
8355
+ }
8356
+ }
8357
+
8327
8358
  if (msg.type === "completion_suggestion_request" && typeof msg.requestId === "string" && typeof msg.text === "string") {
8328
8359
  const textLength = msg.text.length;
8329
8360
  const rawStart = typeof msg.selectionStart === "number" && Number.isFinite(msg.selectionStart) ? msg.selectionStart : textLength;
@@ -10220,7 +10251,7 @@ ${cssVarsBlock}
10220
10251
  <button id="saveOverBtn" type="button" title="Overwrite current file with editor content. Shortcut: Cmd/Ctrl+S.">Save editor</button>
10221
10252
  <button id="refreshFromDiskBtn" type="button" title="Reload the current file-backed document from disk.">Refresh from disk</button>
10222
10253
  <button id="clearWorkspaceBtn" type="button" title="Clear editor text and reset this tab to a fresh blank draft. Saved files and responses are not changed.">Reset editor</button>
10223
- <label class="file-label" title="Browser import: load a selected text file as a detached unsaved copy. It will not be refreshable from disk. Use the Files view to open a file-backed document.">Import file copy…<input id="fileInput" type="file" accept=".md,.markdown,.mdx,.qmd,.js,.mjs,.cjs,.jsx,.ts,.mts,.cts,.tsx,.py,.pyw,.sh,.bash,.zsh,.json,.jsonc,.json5,.rs,.c,.h,.cpp,.cxx,.cc,.hpp,.hxx,.jl,.f90,.f95,.f03,.f,.for,.r,.R,.m,.tex,.latex,.diff,.patch,.java,.go,.rb,.swift,.html,.htm,.css,.xml,.yaml,.yml,.toml,.lua,.txt,.rst,.adoc" /></label>
10254
+ <label class="file-label" title="Browser import: load a selected text file as a detached copy. Use Save editor as… to attach this copy to a file path and make it file-backed, or use the Files view to open a refreshable file-backed document directly.">Import file copy…<input id="fileInput" type="file" accept=".md,.markdown,.mdx,.qmd,.js,.mjs,.cjs,.jsx,.ts,.mts,.cts,.tsx,.py,.pyw,.sh,.bash,.zsh,.json,.jsonc,.json5,.rs,.c,.h,.cpp,.cxx,.cc,.hpp,.hxx,.jl,.f90,.f95,.f03,.f,.for,.r,.R,.m,.tex,.latex,.diff,.patch,.java,.go,.rb,.swift,.html,.htm,.css,.xml,.yaml,.yml,.toml,.lua,.txt,.rst,.adoc" /></label>
10224
10255
  <button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
10225
10256
  <button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide secondary Studio controls. Shortcut: F9.">Zen</button>
10226
10257
  </div>
@@ -10503,7 +10534,8 @@ ${cssVarsBlock}
10503
10534
 
10504
10535
  <footer>
10505
10536
  <span id="statusLine"><span id="statusSpinner" aria-hidden="true"> </span><span id="status">Booting studio…</span></span>
10506
- <span id="footerMeta" class="footer-meta"><span id="footerMetaText" class="footer-meta-text"><span id="footerMetaModel" class="footer-meta-part footer-meta-model">${initialModel}</span><span class="footer-meta-sep">·</span><span id="footerMetaTerminal" class="footer-meta-part footer-meta-terminal">${initialTerminal}</span><span class="footer-meta-sep">·</span><span id="footerMetaContext" class="footer-meta-part footer-meta-context">unknown</span></span><button id="compactBtn" class="footer-compact-btn" type="button" title="Trigger pi context compaction now.">Compact</button></span>
10537
+ <span id="footerMeta" class="footer-meta"><span id="footerMetaText" class="footer-meta-text"><button id="footerMetaModel" class="footer-meta-part footer-meta-model footer-model-btn" type="button" aria-haspopup="menu" aria-expanded="false">${initialModel}</button><span class="footer-meta-sep">·</span><span id="footerMetaTerminal" class="footer-meta-part footer-meta-terminal">${initialTerminal}</span><span class="footer-meta-sep">·</span><span id="footerMetaContext" class="footer-meta-part footer-meta-context">unknown</span></span><button id="compactBtn" class="footer-compact-btn" type="button" title="Trigger pi context compaction now.">Compact</button></span>
10538
+ <div id="footerModelMenu" class="footer-model-menu" hidden></div>
10507
10539
  <button id="shortcutsBtn" class="shortcut-hint" type="button" title="Show Studio keyboard shortcuts. Press ? when not editing text.">Shortcuts (?)</button>
10508
10540
  </footer>
10509
10541
 
@@ -10624,7 +10656,7 @@ export default function (pi: ExtensionAPI) {
10624
10656
  let terminalActivityToolName: string | null = null;
10625
10657
  let terminalActivityLabel: string | null = null;
10626
10658
  let lastSpecificToolActivityLabel: string | null = null;
10627
- let currentModel: { provider?: string; id?: string } | undefined;
10659
+ let currentModel: { provider?: string; id?: string; name?: string; reasoning?: boolean } | undefined;
10628
10660
  let currentModelLabel = "none";
10629
10661
  let terminalSessionLabel = buildTerminalSessionLabel(studioCwd);
10630
10662
  let terminalSessionDetail = buildTerminalSessionDetail(studioCwd);
@@ -10897,15 +10929,20 @@ export default function (pi: ExtensionAPI) {
10897
10929
  }
10898
10930
  };
10899
10931
 
10900
- const getThinkingLevelSafe = (): string | undefined => {
10932
+ const getThinkingLevelSafe = (): ModelThinkingLevel | undefined => {
10901
10933
  try {
10902
- return pi.getThinkingLevel();
10934
+ return pi.getThinkingLevel() as ModelThinkingLevel;
10903
10935
  } catch {
10904
10936
  return undefined;
10905
10937
  }
10906
10938
  };
10907
10939
 
10908
- const refreshRuntimeMetadata = (ctx?: { cwd?: string; model?: { provider?: string; id?: string } | undefined }) => {
10940
+ const setThinkingLevelSafe = (level: ModelThinkingLevel) => {
10941
+ // Pi's CLI/model config support "off" as a thinking level; some extension API typings still expose the narrower reasoning-only type.
10942
+ (pi.setThinkingLevel as (nextLevel: ModelThinkingLevel) => void)(level);
10943
+ };
10944
+
10945
+ const refreshRuntimeMetadata = (ctx?: { cwd?: string; model?: { provider?: string; id?: string; name?: string; reasoning?: boolean } | undefined }) => {
10909
10946
  if (ctx?.cwd) {
10910
10947
  studioCwd = ctx.cwd;
10911
10948
  }
@@ -10914,6 +10951,8 @@ export default function (pi: ExtensionAPI) {
10914
10951
  currentModel = {
10915
10952
  provider: ctx.model.provider,
10916
10953
  id: ctx.model.id,
10954
+ name: ctx.model.name,
10955
+ reasoning: Boolean(ctx.model.reasoning),
10917
10956
  };
10918
10957
  } else {
10919
10958
  currentModel = undefined;
@@ -10922,6 +10961,8 @@ export default function (pi: ExtensionAPI) {
10922
10961
  currentModel = {
10923
10962
  provider: lastCommandCtx.model.provider,
10924
10963
  id: lastCommandCtx.model.id,
10964
+ name: lastCommandCtx.model.name,
10965
+ reasoning: Boolean(lastCommandCtx.model.reasoning),
10925
10966
  };
10926
10967
  }
10927
10968
  const baseModelLabel = formatModelLabel(currentModel);
@@ -11606,7 +11647,7 @@ export default function (pi: ExtensionAPI) {
11606
11647
  broadcastState();
11607
11648
  };
11608
11649
 
11609
- const getSuggestionModelOptions = () => {
11650
+ const getStudioModelOptions = () => {
11610
11651
  const registry = lastCommandCtx?.modelRegistry ?? latestModelRequestCtx?.modelRegistry;
11611
11652
  if (!registry || typeof registry.getAvailable !== "function") return [];
11612
11653
  return registry.getAvailable().map((model) => ({
@@ -11617,11 +11658,21 @@ export default function (pi: ExtensionAPI) {
11617
11658
  }));
11618
11659
  };
11619
11660
 
11661
+ const getCurrentStudioModelDescriptor = () => currentModel
11662
+ ? {
11663
+ provider: currentModel.provider,
11664
+ id: currentModel.id,
11665
+ label: formatStudioModelOptionLabel(currentModel),
11666
+ reasoning: Boolean(currentModel.reasoning),
11667
+ }
11668
+ : null;
11669
+
11620
11670
  const broadcastState = () => {
11621
11671
  terminalSessionLabel = buildTerminalSessionLabel(studioCwd, getSessionNameSafe());
11622
11672
  terminalSessionDetail = buildTerminalSessionDetail(studioCwd, getSessionNameSafe());
11623
11673
  currentModelLabel = formatModelLabelWithThinking(formatModelLabel(currentModel), getThinkingLevelSafe());
11624
11674
  refreshContextUsage();
11675
+ const modelOptions = getStudioModelOptions();
11625
11676
  broadcast({
11626
11677
  type: "studio_state",
11627
11678
  busy: isStudioBusy(),
@@ -11630,7 +11681,10 @@ export default function (pi: ExtensionAPI) {
11630
11681
  terminalToolName: terminalActivityToolName,
11631
11682
  terminalActivityLabel,
11632
11683
  modelLabel: currentModelLabel,
11633
- suggestionModels: getSuggestionModelOptions(),
11684
+ currentModel: getCurrentStudioModelDescriptor(),
11685
+ thinkingLevel: getThinkingLevelSafe() ?? "off",
11686
+ piModels: modelOptions,
11687
+ suggestionModels: modelOptions,
11634
11688
  terminalSessionLabel,
11635
11689
  terminalSessionDetail,
11636
11690
  contextTokens: contextUsageSnapshot.tokens,
@@ -11926,7 +11980,10 @@ export default function (pi: ExtensionAPI) {
11926
11980
  terminalToolName: terminalActivityToolName,
11927
11981
  terminalActivityLabel,
11928
11982
  modelLabel: currentModelLabel,
11929
- suggestionModels: getSuggestionModelOptions(),
11983
+ currentModel: getCurrentStudioModelDescriptor(),
11984
+ thinkingLevel: getThinkingLevelSafe() ?? "off",
11985
+ piModels: getStudioModelOptions(),
11986
+ suggestionModels: getStudioModelOptions(),
11930
11987
  terminalSessionLabel,
11931
11988
  terminalSessionDetail,
11932
11989
  contextTokens: contextUsageSnapshot.tokens,
@@ -11945,6 +12002,47 @@ export default function (pi: ExtensionAPI) {
11945
12002
  return;
11946
12003
  }
11947
12004
 
12005
+ if (msg.type === "pi_model_select_request") {
12006
+ void (async () => {
12007
+ const registry = lastCommandCtx?.modelRegistry ?? latestModelRequestCtx?.modelRegistry;
12008
+ if (!registry || typeof registry.find !== "function") {
12009
+ sendToClient(client, { type: "info", level: "warning", message: "Pi model registry is not available yet." });
12010
+ return;
12011
+ }
12012
+ const model = registry.find(msg.provider, msg.id);
12013
+ if (!model) {
12014
+ sendToClient(client, { type: "info", level: "warning", message: `Pi model not found: ${msg.provider}/${msg.id}` });
12015
+ return;
12016
+ }
12017
+ try {
12018
+ const ok = await pi.setModel(model);
12019
+ if (!ok) {
12020
+ sendToClient(client, { type: "info", level: "warning", message: `Could not switch to ${formatStudioModelOptionLabel(model)}; credentials may be unavailable.` });
12021
+ return;
12022
+ }
12023
+ latestModelRequestCtx = { model, modelRegistry: registry };
12024
+ refreshRuntimeMetadata({ model });
12025
+ broadcastState();
12026
+ sendToClient(client, { type: "info", level: "info", message: `Pi model switched to ${formatStudioModelOptionLabel(model)}.` });
12027
+ } catch (error) {
12028
+ sendToClient(client, { type: "info", level: "error", message: `Model switch failed: ${error instanceof Error ? error.message : String(error)}` });
12029
+ }
12030
+ })();
12031
+ return;
12032
+ }
12033
+
12034
+ if (msg.type === "pi_thinking_level_request") {
12035
+ try {
12036
+ setThinkingLevelSafe(msg.level);
12037
+ refreshRuntimeMetadata({ model: lastCommandCtx?.model ?? latestModelRequestCtx?.model });
12038
+ broadcastState();
12039
+ sendToClient(client, { type: "info", level: "info", message: `Pi thinking level set to ${getThinkingLevelSafe() ?? msg.level}.` });
12040
+ } catch (error) {
12041
+ sendToClient(client, { type: "info", level: "error", message: `Thinking level change failed: ${error instanceof Error ? error.message : String(error)}` });
12042
+ }
12043
+ return;
12044
+ }
12045
+
11948
12046
  if (msg.type === "get_latest_response") {
11949
12047
  if (!lastStudioResponse) {
11950
12048
  sendToClient(client, { type: "info", message: "No latest assistant response is available yet." });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.9.27",
3
+ "version": "0.9.28",
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",