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 +9 -0
- package/client/studio-client.js +154 -3
- package/client/studio.css +89 -3
- package/index.ts +108 -10
- package/package.json +1 -1
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
|
package/client/studio-client.js
CHANGED
|
@@ -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", "
|
|
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", "
|
|
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 = "
|
|
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
|
|
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"><
|
|
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 = ():
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|