pi-studio 0.9.26 → 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 +27 -0
- package/README.md +1 -1
- package/client/studio-client.js +338 -16
- package/client/studio.css +97 -2
- package/index.ts +328 -41
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `pi-studio` are documented here.
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
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
|
+
|
|
16
|
+
## [0.9.27] — 2026-06-08
|
|
17
|
+
|
|
18
|
+
### Added
|
|
19
|
+
- Added an **Open root** action to the Files view for opening the Files root folder in Finder or the system file manager.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Clarified browser-import and Files-view file-backed open action tooltips.
|
|
23
|
+
- Added **Try another** regeneration for completion suggestions and a persistent Suggestion model picker that keeps suggestions separate from the main Pi model and uses thinking off, with clearer cursor-marker prompting and separate prose/code completion instructions.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- Avoided showing the missing-LaTeX-engine hint for PDF exports when XeLaTeX/PDFLaTeX ran but the document itself had a LaTeX error.
|
|
27
|
+
- Restored Pandoc-rendered LaTeX title/author/abstract metadata in Studio previews that use embedded resources.
|
|
28
|
+
- Preserved optional LaTeX `\footnotemark[n]` markers as linked superscript affiliation markers in Studio previews.
|
|
29
|
+
- Nudged inline completion suggestions to return a non-empty continuation when explicitly requested at the end of a sentence or paragraph.
|
|
30
|
+
- Fixed Markdown blockquotes containing multiline `\[ ... \]` display math so quote markers are not folded into the math content.
|
|
31
|
+
|
|
5
32
|
## [0.9.26] — 2026-06-04
|
|
6
33
|
|
|
7
34
|
### Fixed
|
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
|
|
|
24
24
|
- Runs editor text directly, asks for structured critique (auto/writing/code focus), offers a manual **Suggest completion** action for short cursor-aware continuations (`Option/Alt+Tab` where available or `Cmd/Ctrl+Shift+Space` from the editor, `Tab` to insert a visible suggestion) with an optional editor-plus-latest-response context mode, or opens **Quiz me** for a Studio-native active-recall loop over the current editor text, selection, current file, folder, or repo, with optional focus guidance for shaping question selection
|
|
25
25
|
- Includes a live **Working** view for following current model/tool activity, with `All` / `Thinking` / `Tools` filters, image previews for image-producing tool outputs, plus **Load visible into editor** and **Copy visible** actions; when cycling response history, Working follows saved working details for the selected response when available
|
|
26
26
|
- Includes a right-pane **Changes** view for browsing the current git diff by file, previewing per-file diffs, opening changed files, loading the full diff into the editor, and copying the diff
|
|
27
|
-
- Includes a right-pane **Files** view for browsing the current Pi session/resource directory, opening folders, loading text/code/CSV/TSV documents into the editor, previewing PDFs/images, opening PDF/image previews in a new Studio tab, converting DOCX/ODT documents to editable Markdown when Pandoc is available after confirmation, copying paths, setting the current folder as the Studio working directory, and revealing files in the file manager
|
|
27
|
+
- Includes a right-pane **Files** view for browsing the current Pi session/resource directory, opening folders, opening the Files root in Finder/the system file manager, loading text/code/CSV/TSV documents into the editor, previewing PDFs/images, opening PDF/image previews in a new Studio tab, converting DOCX/ODT documents to editable Markdown when Pandoc is available after confirmation, copying paths, setting the current folder as the Studio working directory, and revealing files in the file manager
|
|
28
28
|
- Includes an optional tmux-backed **REPL** view for Shell, Python, IPython, Julia, R, GHCi, and Clojure sessions, with Raw/Literate send modes, `Cmd/Ctrl+Shift+Enter` **Send to REPL**, session start/stop/interrupt controls, a compact refresh-persistent **Studio REPL Record** of user and Pi-sent code, a secondary raw tmux mirror, agent-facing `studio_repl_status` / `studio_repl_send` tools, and Markdown/PDF/HTML export
|
|
29
29
|
- Includes a local persistent scratchpad for quick notes you want to keep out of the main editor until you're ready to copy or insert them, with a **Recent…** picker for recovering scratchpads saved under earlier file/draft identities
|
|
30
30
|
- Includes a docked **Outline** rail for navigating document structure in the current editor text, with clickable entries that jump in the raw editor and reveal matching preview locations when available
|
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");
|
|
@@ -118,8 +119,11 @@
|
|
|
118
119
|
const suggestCompletionBtn = document.getElementById("suggestCompletionBtn");
|
|
119
120
|
const suggestCompletionOptionsBtn = document.getElementById("suggestCompletionOptionsBtn");
|
|
120
121
|
const completionContextSelect = document.getElementById("completionContextSelect");
|
|
122
|
+
const completionModelSelect = document.getElementById("completionModelSelect");
|
|
121
123
|
const completionSuggestionPanelEl = document.getElementById("completionSuggestionPanel");
|
|
122
124
|
const completionSuggestionTextEl = document.getElementById("completionSuggestionText");
|
|
125
|
+
const completionSuggestionMetaEl = document.getElementById("completionSuggestionMeta");
|
|
126
|
+
const completionSuggestionRegenerateBtn = document.getElementById("completionSuggestionRegenerateBtn");
|
|
123
127
|
const completionSuggestionInsertBtn = document.getElementById("completionSuggestionInsertBtn");
|
|
124
128
|
const completionSuggestionDismissBtn = document.getElementById("completionSuggestionDismissBtn");
|
|
125
129
|
const saveAnnotatedBtn = document.getElementById("saveAnnotatedBtn");
|
|
@@ -325,6 +329,7 @@
|
|
|
325
329
|
const EDITOR_TAB_TEXT = " ";
|
|
326
330
|
const QUIZ_DEFAULT_COUNT = 5;
|
|
327
331
|
const COMPLETION_CONTEXT_STORAGE_KEY = "piStudio.completionContextMode";
|
|
332
|
+
const COMPLETION_MODEL_STORAGE_KEY = "piStudio.completionModel";
|
|
328
333
|
const COMPLETION_CONTEXT_MAX_CHARS = 12000;
|
|
329
334
|
const QUIZ_SCOPES = ["editor", "selection", "file", "folder", "repo"];
|
|
330
335
|
const QUIZ_ANGLES = ["general", "scientist", "mathematician", "statistician", "developer", "reviewer"];
|
|
@@ -518,6 +523,10 @@
|
|
|
518
523
|
let previewExportInProgress = false;
|
|
519
524
|
let compactInProgress = false;
|
|
520
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;
|
|
521
530
|
let terminalSessionLabel = (document.body && document.body.dataset && document.body.dataset.terminalLabel) || "unknown";
|
|
522
531
|
let terminalSessionDetail = (document.body && document.body.dataset && document.body.dataset.terminalDetail) || terminalSessionLabel;
|
|
523
532
|
let contextTokens = null;
|
|
@@ -2027,6 +2036,8 @@
|
|
|
2027
2036
|
let responseHighlightEnabled = false;
|
|
2028
2037
|
let completionSuggestionState = null;
|
|
2029
2038
|
let completionSuggestionContextMode = readCompletionSuggestionContextMode();
|
|
2039
|
+
let completionSuggestionModelValue = readCompletionSuggestionModelValue();
|
|
2040
|
+
let completionSuggestionModelOptions = [];
|
|
2030
2041
|
let completionSuggestionInFlight = false;
|
|
2031
2042
|
let completionSuggestionRequestId = null;
|
|
2032
2043
|
let completionSuggestionPendingSnapshot = null;
|
|
@@ -2499,10 +2510,18 @@
|
|
|
2499
2510
|
syncActionButtons();
|
|
2500
2511
|
});
|
|
2501
2512
|
});
|
|
2502
|
-
|
|
2513
|
+
const suggestionItems = [cursorContextBtn, sessionContextBtn];
|
|
2514
|
+
if (completionModelSelect) {
|
|
2515
|
+
completionModelSelect.hidden = false;
|
|
2516
|
+
suggestionItems.push(completionModelSelect);
|
|
2517
|
+
}
|
|
2518
|
+
const completionThinkingNoteEl = makeStudioUiRefreshElement("div", "studio-refresh-menu-note completion-thinking-note", "Suggest: thinking off; model choice only affects suggestions.");
|
|
2519
|
+
completionThinkingNoteEl.setAttribute("aria-label", "Suggestion model note");
|
|
2520
|
+
suggestionItems.push(completionThinkingNoteEl);
|
|
2521
|
+
appendStudioUiRefreshMenuSection(contextMenu.menu, "Suggestions", suggestionItems);
|
|
2503
2522
|
const statusItems = [];
|
|
2504
2523
|
if (!isEditorOnlyMode) {
|
|
2505
|
-
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.");
|
|
2506
2525
|
sourceSessionSummaryEl.setAttribute("aria-label", "Pi session tree and editor sync behaviour");
|
|
2507
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.";
|
|
2508
2527
|
statusItems.push(sourceSessionSummaryEl);
|
|
@@ -3165,6 +3184,118 @@
|
|
|
3165
3184
|
}
|
|
3166
3185
|
}
|
|
3167
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
|
+
|
|
3168
3299
|
function updateFooterMeta() {
|
|
3169
3300
|
const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
|
|
3170
3301
|
const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
|
|
@@ -3178,7 +3309,9 @@
|
|
|
3178
3309
|
footerMetaModelEl.textContent = modelText;
|
|
3179
3310
|
footerMetaTerminalEl.textContent = terminalText;
|
|
3180
3311
|
footerMetaContextEl.textContent = contextDisplayText;
|
|
3181
|
-
footerMetaModelEl.title = "
|
|
3312
|
+
footerMetaModelEl.title = "Pi model and thinking: " + modelText;
|
|
3313
|
+
footerMetaModelEl.setAttribute("aria-haspopup", "menu");
|
|
3314
|
+
footerMetaModelEl.setAttribute("aria-expanded", footerModelMenuOpen ? "true" : "false");
|
|
3182
3315
|
footerMetaTerminalEl.title = terminalDetailText;
|
|
3183
3316
|
footerMetaContextEl.title = contextTitleText;
|
|
3184
3317
|
if (footerMetaTextEl) footerMetaTextEl.title = titleText;
|
|
@@ -9661,14 +9794,17 @@
|
|
|
9661
9794
|
? "open-new"
|
|
9662
9795
|
: ((kind === "pdf" || kind === "image") ? "open-preview-new" : "");
|
|
9663
9796
|
const newTabLabel = kind === "text"
|
|
9664
|
-
? "Open file tab"
|
|
9797
|
+
? "Open file-backed tab"
|
|
9665
9798
|
: (kind === "office" ? "Convert tab" : ((kind === "pdf" || kind === "image") ? "Preview tab" : "New tab"));
|
|
9799
|
+
const newTabTitle = kind === "text"
|
|
9800
|
+
? "Open this file-backed document in a new refreshable editor tab. Save editor and Refresh from disk will use this file."
|
|
9801
|
+
: (kind === "office" ? "Convert this document to Markdown in a new editor tab." : ((kind === "pdf" || kind === "image") ? "Open this preview in a new Studio tab." : "Open in a new Studio tab."));
|
|
9666
9802
|
const textActions = newTabAction
|
|
9667
|
-
? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(entry.path) + "'>" + escapeHtml(newTabLabel) + "</button>"
|
|
9803
|
+
? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(entry.path) + "' title='" + escapeHtml(newTabTitle) + "'>" + escapeHtml(newTabLabel) + "</button>"
|
|
9668
9804
|
: "";
|
|
9669
9805
|
const openTitle = type === "directory"
|
|
9670
9806
|
? "Open folder"
|
|
9671
|
-
: (kind === "text" ? "Open in editor" : (kind === "office" ? "Convert to Markdown in editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file"))));
|
|
9807
|
+
: (kind === "text" ? "Open file-backed document in the current editor. Save editor and Refresh from disk will use this file." : (kind === "office" ? "Convert to Markdown in the current editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file"))));
|
|
9672
9808
|
return "<div class='files-row files-row-" + escapeHtml(type) + " files-kind-" + escapeHtml(kind) + "'>"
|
|
9673
9809
|
+ "<button type='button' class='files-open-btn' data-files-action='" + (type === "directory" ? "open-dir" : "open") + "' data-files-path='" + escapeHtml(entry.path) + "' data-files-kind='" + escapeHtml(kind) + "' title='" + escapeHtml(openTitle) + "'>"
|
|
9674
9810
|
+ "<span class='files-icon' aria-hidden='true'>" + icon + "</span>"
|
|
@@ -9695,6 +9831,7 @@
|
|
|
9695
9831
|
+ "<button type='button' data-files-action='refresh'>Refresh</button>"
|
|
9696
9832
|
+ (currentDir ? "<button type='button' data-files-action='copy-current' data-files-path='" + escapeHtml(currentDir) + "'>Copy path</button>" : "")
|
|
9697
9833
|
+ (currentDir ? "<button type='button' data-files-action='use-working-dir' data-files-path='" + escapeHtml(currentDir) + "'>Use as working dir</button>" : "")
|
|
9834
|
+
+ (rootDir ? "<button type='button' data-files-action='open-root' data-files-path='" + escapeHtml(rootDir) + "' title='Open the Files root folder in Finder or the system file manager.'>Open root</button>" : "")
|
|
9698
9835
|
+ (rootDir ? "<button type='button' data-files-action='copy-root' data-files-path='" + escapeHtml(rootDir) + "'>Copy root</button>" : "")
|
|
9699
9836
|
+ "</div>"
|
|
9700
9837
|
+ "</div>"
|
|
@@ -9845,6 +9982,23 @@
|
|
|
9845
9982
|
setStatus("Working dir set to current folder.", "success");
|
|
9846
9983
|
}
|
|
9847
9984
|
|
|
9985
|
+
async function openFileBrowserDirectoryInFileViewer(path) {
|
|
9986
|
+
const targetDir = normalizeStudioResourceDirValue(path || fileBrowserState.rootDir || fileBrowserState.currentDir || "");
|
|
9987
|
+
if (!targetDir) {
|
|
9988
|
+
setStatus("No folder to open.", "warning");
|
|
9989
|
+
return;
|
|
9990
|
+
}
|
|
9991
|
+
const context = getHtmlPreviewResourceContextOptions();
|
|
9992
|
+
const body = { dir: targetDir };
|
|
9993
|
+
if (context.sourcePath) body.sourcePath = context.sourcePath;
|
|
9994
|
+
if (context.resourceDir) body.resourceDir = context.resourceDir;
|
|
9995
|
+
const payload = await fetchStudioJson("/file-browser-open", {
|
|
9996
|
+
method: "POST",
|
|
9997
|
+
body: JSON.stringify(body),
|
|
9998
|
+
});
|
|
9999
|
+
setStatus(payload && payload.message ? payload.message : "Opened folder in file manager.", "success");
|
|
10000
|
+
}
|
|
10001
|
+
|
|
9848
10002
|
async function handleFilesPaneClick(event) {
|
|
9849
10003
|
if (rightView !== "files") return;
|
|
9850
10004
|
const target = event.target;
|
|
@@ -9888,6 +10042,10 @@
|
|
|
9888
10042
|
setFileBrowserCurrentDirectoryAsWorkingDir(path);
|
|
9889
10043
|
return;
|
|
9890
10044
|
}
|
|
10045
|
+
if (action === "open-root") {
|
|
10046
|
+
await openFileBrowserDirectoryInFileViewer(path || fileBrowserState.rootDir || "");
|
|
10047
|
+
return;
|
|
10048
|
+
}
|
|
9891
10049
|
if (action === "reveal") {
|
|
9892
10050
|
await revealPreviewLocalLink(path, getFileBrowserLocalLinkContext());
|
|
9893
10051
|
}
|
|
@@ -10437,13 +10595,18 @@
|
|
|
10437
10595
|
syncRunAndCritiqueButtons();
|
|
10438
10596
|
copyDraftBtn.disabled = uiBusy;
|
|
10439
10597
|
if (suggestCompletionBtn) {
|
|
10598
|
+
const hasSuggestionForCurrentText = Boolean(completionSuggestionState && sourceTextEl && sourceTextEl.value === completionSuggestionState.baseText);
|
|
10440
10599
|
suggestCompletionBtn.disabled = wsState !== "Ready" || (!completionSuggestionInFlight && (uiBusy || !String(sourceTextEl.value || "").trim()));
|
|
10441
|
-
suggestCompletionBtn.textContent = completionSuggestionInFlight ? "Stop" : "Suggest";
|
|
10600
|
+
suggestCompletionBtn.textContent = completionSuggestionInFlight ? "Stop" : (hasSuggestionForCurrentText ? "Try another" : "Suggest");
|
|
10442
10601
|
suggestCompletionBtn.title = completionSuggestionInFlight
|
|
10443
10602
|
? "Stop the current suggestion request."
|
|
10444
|
-
:
|
|
10603
|
+
: (hasSuggestionForCurrentText
|
|
10604
|
+
? "Ask for a different suggestion at the same cursor position."
|
|
10605
|
+
: "Ask for a short completion at the editor cursor. Shortcut: Option/Alt+Tab where available, or Cmd/Ctrl+Shift+Space from the editor.");
|
|
10445
10606
|
}
|
|
10446
10607
|
if (suggestCompletionOptionsBtn) suggestCompletionOptionsBtn.disabled = uiBusy || completionSuggestionInFlight;
|
|
10608
|
+
if (completionModelSelect) completionModelSelect.disabled = uiBusy || completionSuggestionInFlight;
|
|
10609
|
+
if (completionSuggestionRegenerateBtn) completionSuggestionRegenerateBtn.disabled = completionSuggestionInFlight || !completionSuggestionState;
|
|
10447
10610
|
syncCompletionSuggestionContextUi();
|
|
10448
10611
|
if (openCompanionBtn) openCompanionBtn.disabled = uiBusy || wsState !== "Ready";
|
|
10449
10612
|
if (highlightSelect) highlightSelect.disabled = uiBusy;
|
|
@@ -10784,9 +10947,37 @@
|
|
|
10784
10947
|
}
|
|
10785
10948
|
}
|
|
10786
10949
|
|
|
10950
|
+
function readCompletionSuggestionModelValue() {
|
|
10951
|
+
try {
|
|
10952
|
+
const stored = window.localStorage ? String(window.localStorage.getItem(COMPLETION_MODEL_STORAGE_KEY) || "") : "";
|
|
10953
|
+
return stored && stored !== "undefined" && stored !== "null" ? stored : "current";
|
|
10954
|
+
} catch {
|
|
10955
|
+
return "current";
|
|
10956
|
+
}
|
|
10957
|
+
}
|
|
10958
|
+
|
|
10959
|
+
function encodeCompletionModelValue(provider, id) {
|
|
10960
|
+
return JSON.stringify([String(provider || ""), String(id || "")]);
|
|
10961
|
+
}
|
|
10962
|
+
|
|
10963
|
+
function decodeCompletionModelValue(value) {
|
|
10964
|
+
const raw = String(value || "");
|
|
10965
|
+
if (!raw || raw === "current") return null;
|
|
10966
|
+
try {
|
|
10967
|
+
const parsed = JSON.parse(raw);
|
|
10968
|
+
if (!Array.isArray(parsed) || parsed.length < 2) return null;
|
|
10969
|
+
const provider = String(parsed[0] || "").trim();
|
|
10970
|
+
const id = String(parsed[1] || "").trim();
|
|
10971
|
+
return provider && id ? { provider, id } : null;
|
|
10972
|
+
} catch {
|
|
10973
|
+
return null;
|
|
10974
|
+
}
|
|
10975
|
+
}
|
|
10976
|
+
|
|
10787
10977
|
function setCompletionSuggestionContextMode(mode) {
|
|
10788
10978
|
completionSuggestionContextMode = mode === "session" ? "session" : "cursor";
|
|
10789
10979
|
if (completionContextSelect) completionContextSelect.value = completionSuggestionContextMode;
|
|
10980
|
+
hideCompletionSuggestion();
|
|
10790
10981
|
try {
|
|
10791
10982
|
if (window.localStorage) window.localStorage.setItem(COMPLETION_CONTEXT_STORAGE_KEY, completionSuggestionContextMode);
|
|
10792
10983
|
} catch {}
|
|
@@ -10795,8 +10986,65 @@
|
|
|
10795
10986
|
: "Suggestions will use cursor-local editor context only.");
|
|
10796
10987
|
}
|
|
10797
10988
|
|
|
10989
|
+
function setCompletionSuggestionModelValue(value) {
|
|
10990
|
+
const normalized = String(value || "current") || "current";
|
|
10991
|
+
completionSuggestionModelValue = normalized;
|
|
10992
|
+
if (completionModelSelect) completionModelSelect.value = normalized;
|
|
10993
|
+
hideCompletionSuggestion();
|
|
10994
|
+
try {
|
|
10995
|
+
if (window.localStorage) window.localStorage.setItem(COMPLETION_MODEL_STORAGE_KEY, normalized);
|
|
10996
|
+
} catch {}
|
|
10997
|
+
const selectedLabel = getCompletionSuggestionModelLabel();
|
|
10998
|
+
setStatus(normalized === "current"
|
|
10999
|
+
? "Suggestions will use the current Pi model with thinking off."
|
|
11000
|
+
: "Suggestions will use " + selectedLabel + " with thinking off.");
|
|
11001
|
+
}
|
|
11002
|
+
|
|
11003
|
+
function getCompletionSuggestionModelSelection() {
|
|
11004
|
+
return decodeCompletionModelValue(completionSuggestionModelValue);
|
|
11005
|
+
}
|
|
11006
|
+
|
|
11007
|
+
function getCompletionSuggestionModelLabel() {
|
|
11008
|
+
const selected = getCompletionSuggestionModelSelection();
|
|
11009
|
+
if (!selected) return "current Pi model";
|
|
11010
|
+
const match = completionSuggestionModelOptions.find((option) => option.provider === selected.provider && option.id === selected.id);
|
|
11011
|
+
return match && match.label ? match.label : (selected.provider + "/" + selected.id);
|
|
11012
|
+
}
|
|
11013
|
+
|
|
11014
|
+
function updateCompletionSuggestionModelOptions(options) {
|
|
11015
|
+
completionSuggestionModelOptions = Array.isArray(options)
|
|
11016
|
+
? options.map((option) => ({
|
|
11017
|
+
provider: String(option && option.provider || "").trim(),
|
|
11018
|
+
id: String(option && option.id || "").trim(),
|
|
11019
|
+
label: String(option && option.label || "").trim(),
|
|
11020
|
+
reasoning: Boolean(option && option.reasoning),
|
|
11021
|
+
})).filter((option) => option.provider && option.id)
|
|
11022
|
+
: [];
|
|
11023
|
+
syncCompletionSuggestionModelUi();
|
|
11024
|
+
}
|
|
11025
|
+
|
|
11026
|
+
function syncCompletionSuggestionModelUi() {
|
|
11027
|
+
if (!completionModelSelect) return;
|
|
11028
|
+
const currentValue = completionSuggestionModelValue || "current";
|
|
11029
|
+
const modelOptionsHtml = completionSuggestionModelOptions.map((option) => {
|
|
11030
|
+
const value = encodeCompletionModelValue(option.provider, option.id);
|
|
11031
|
+
const label = option.label || (option.provider + "/" + option.id);
|
|
11032
|
+
return "<option value='" + escapeHtml(value) + "'>Suggestion model: " + escapeHtml(label) + "</option>";
|
|
11033
|
+
});
|
|
11034
|
+
const validValues = new Set(["current", ...completionSuggestionModelOptions.map((option) => encodeCompletionModelValue(option.provider, option.id))]);
|
|
11035
|
+
if (!validValues.has(currentValue) && completionSuggestionModelOptions.length === 0 && currentValue !== "current") {
|
|
11036
|
+
modelOptionsHtml.push("<option value='" + escapeHtml(currentValue) + "'>Suggestion model: saved selection</option>");
|
|
11037
|
+
validValues.add(currentValue);
|
|
11038
|
+
}
|
|
11039
|
+
completionModelSelect.innerHTML = ["<option value='current'>Suggestion model: current Pi model</option>", ...modelOptionsHtml].join("");
|
|
11040
|
+
completionModelSelect.value = validValues.has(currentValue) ? currentValue : "current";
|
|
11041
|
+
if (completionModelSelect.value !== currentValue) completionSuggestionModelValue = "current";
|
|
11042
|
+
completionModelSelect.title = "Choose the model used for Suggest. Suggestions use direct completion with thinking off and do not change the main Pi model.";
|
|
11043
|
+
}
|
|
11044
|
+
|
|
10798
11045
|
function syncCompletionSuggestionContextUi() {
|
|
10799
11046
|
if (completionContextSelect) completionContextSelect.value = completionSuggestionContextMode;
|
|
11047
|
+
syncCompletionSuggestionModelUi();
|
|
10800
11048
|
if (suggestCompletionOptionsBtn) {
|
|
10801
11049
|
suggestCompletionOptionsBtn.textContent = "Source & context";
|
|
10802
11050
|
suggestCompletionOptionsBtn.title = completionSuggestionContextMode === "session"
|
|
@@ -10841,13 +11089,20 @@
|
|
|
10841
11089
|
function hideCompletionSuggestion() {
|
|
10842
11090
|
completionSuggestionState = null;
|
|
10843
11091
|
if (completionSuggestionTextEl) completionSuggestionTextEl.textContent = "";
|
|
11092
|
+
if (completionSuggestionMetaEl) completionSuggestionMetaEl.textContent = "";
|
|
10844
11093
|
if (completionSuggestionPanelEl) completionSuggestionPanelEl.hidden = true;
|
|
11094
|
+
syncActionButtons();
|
|
10845
11095
|
}
|
|
10846
11096
|
|
|
10847
11097
|
function showCompletionSuggestion(state) {
|
|
10848
11098
|
completionSuggestionState = state;
|
|
10849
11099
|
if (completionSuggestionTextEl) completionSuggestionTextEl.textContent = state && state.suggestion ? state.suggestion : "";
|
|
11100
|
+
if (completionSuggestionMetaEl) {
|
|
11101
|
+
const modelLabelText = state && state.modelLabel ? String(state.modelLabel) : getCompletionSuggestionModelLabel();
|
|
11102
|
+
completionSuggestionMetaEl.textContent = modelLabelText ? " · " + modelLabelText + " · thinking off" : " · thinking off";
|
|
11103
|
+
}
|
|
10850
11104
|
if (completionSuggestionPanelEl) completionSuggestionPanelEl.hidden = false;
|
|
11105
|
+
syncActionButtons();
|
|
10851
11106
|
}
|
|
10852
11107
|
|
|
10853
11108
|
function focusSourceTextNoScroll() {
|
|
@@ -10927,29 +11182,38 @@
|
|
|
10927
11182
|
}
|
|
10928
11183
|
}
|
|
10929
11184
|
|
|
10930
|
-
function requestCompletionSuggestion() {
|
|
11185
|
+
function requestCompletionSuggestion(options) {
|
|
10931
11186
|
if (isEditorOnlyMode && !sourceTextEl) return;
|
|
10932
11187
|
if (completionSuggestionInFlight) {
|
|
10933
11188
|
cancelCompletionSuggestion();
|
|
10934
11189
|
return;
|
|
10935
11190
|
}
|
|
10936
|
-
const
|
|
11191
|
+
const regenerateRequested = Boolean((options && options.regenerate) || (completionSuggestionState && sourceTextEl.value === completionSuggestionState.baseText));
|
|
11192
|
+
const existingSuggestion = regenerateRequested ? completionSuggestionState : null;
|
|
11193
|
+
const text = existingSuggestion ? String(existingSuggestion.baseText || "") : String(sourceTextEl.value || "");
|
|
10937
11194
|
if (!text.trim()) {
|
|
10938
11195
|
setStatus("Editor is empty.", "warning");
|
|
10939
11196
|
return;
|
|
10940
11197
|
}
|
|
10941
|
-
|
|
10942
|
-
|
|
11198
|
+
if (existingSuggestion && String(sourceTextEl.value || "") !== text) {
|
|
11199
|
+
setStatus("Editor changed. Request a fresh suggestion from the current cursor.", "warning");
|
|
11200
|
+
hideCompletionSuggestion();
|
|
11201
|
+
return;
|
|
11202
|
+
}
|
|
11203
|
+
const selectionStart = existingSuggestion ? existingSuggestion.selectionStart : (typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : text.length);
|
|
11204
|
+
const selectionEnd = existingSuggestion ? existingSuggestion.selectionEnd : (typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : selectionStart);
|
|
10943
11205
|
const contextText = getCompletionSuggestionContextText();
|
|
11206
|
+
const selectedModel = getCompletionSuggestionModelSelection();
|
|
10944
11207
|
const requestId = makeRequestId();
|
|
11208
|
+
const previousSuggestion = existingSuggestion && existingSuggestion.suggestion ? String(existingSuggestion.suggestion) : "";
|
|
10945
11209
|
completionSuggestionInFlight = true;
|
|
10946
11210
|
completionSuggestionRequestId = requestId;
|
|
10947
|
-
completionSuggestionPendingSnapshot = { text, selectionStart, selectionEnd };
|
|
11211
|
+
completionSuggestionPendingSnapshot = { text, selectionStart, selectionEnd, previousSuggestion };
|
|
10948
11212
|
completionSuggestionRefocusEditorOnResult = shouldRefocusEditorForCompletionRequest();
|
|
10949
11213
|
hideCompletionSuggestion();
|
|
10950
11214
|
syncActionButtons();
|
|
10951
|
-
setStatus("Generating completion suggestion…", "warning");
|
|
10952
|
-
const
|
|
11215
|
+
setStatus(existingSuggestion ? "Generating another suggestion…" : "Generating completion suggestion…", "warning");
|
|
11216
|
+
const message = {
|
|
10953
11217
|
type: "completion_suggestion_request",
|
|
10954
11218
|
requestId,
|
|
10955
11219
|
text,
|
|
@@ -10960,7 +11224,13 @@
|
|
|
10960
11224
|
path: sourceState && sourceState.path ? sourceState.path : undefined,
|
|
10961
11225
|
contextMode: completionSuggestionContextMode,
|
|
10962
11226
|
contextText: contextText || undefined,
|
|
10963
|
-
|
|
11227
|
+
previousSuggestion: previousSuggestion || undefined,
|
|
11228
|
+
};
|
|
11229
|
+
if (selectedModel) {
|
|
11230
|
+
message.suggestionModelProvider = selectedModel.provider;
|
|
11231
|
+
message.suggestionModelId = selectedModel.id;
|
|
11232
|
+
}
|
|
11233
|
+
const sent = sendMessage(message);
|
|
10964
11234
|
if (!sent) {
|
|
10965
11235
|
completionSuggestionInFlight = false;
|
|
10966
11236
|
completionSuggestionRequestId = null;
|
|
@@ -11035,6 +11305,8 @@
|
|
|
11035
11305
|
baseText,
|
|
11036
11306
|
selectionStart: start,
|
|
11037
11307
|
selectionEnd: end,
|
|
11308
|
+
previousSuggestion: pendingSnapshot && pendingSnapshot.previousSuggestion ? pendingSnapshot.previousSuggestion : "",
|
|
11309
|
+
modelLabel: typeof message.modelLabel === "string" ? message.modelLabel : getCompletionSuggestionModelLabel(),
|
|
11038
11310
|
});
|
|
11039
11311
|
const activeEl = document.activeElement;
|
|
11040
11312
|
if (
|
|
@@ -18493,6 +18765,10 @@
|
|
|
18493
18765
|
if (typeof message.modelLabel === "string") {
|
|
18494
18766
|
modelLabel = message.modelLabel;
|
|
18495
18767
|
}
|
|
18768
|
+
if (Array.isArray(message.suggestionModels)) {
|
|
18769
|
+
updateCompletionSuggestionModelOptions(message.suggestionModels);
|
|
18770
|
+
}
|
|
18771
|
+
updatePiSessionModelState(message);
|
|
18496
18772
|
if (typeof message.terminalSessionLabel === "string") {
|
|
18497
18773
|
terminalSessionLabel = message.terminalSessionLabel;
|
|
18498
18774
|
}
|
|
@@ -19065,6 +19341,10 @@
|
|
|
19065
19341
|
if (typeof message.modelLabel === "string") {
|
|
19066
19342
|
modelLabel = message.modelLabel;
|
|
19067
19343
|
}
|
|
19344
|
+
if (Array.isArray(message.suggestionModels)) {
|
|
19345
|
+
updateCompletionSuggestionModelOptions(message.suggestionModels);
|
|
19346
|
+
}
|
|
19347
|
+
updatePiSessionModelState(message);
|
|
19068
19348
|
if (typeof message.terminalSessionLabel === "string") {
|
|
19069
19349
|
terminalSessionLabel = message.terminalSessionLabel;
|
|
19070
19350
|
}
|
|
@@ -19944,9 +20224,39 @@
|
|
|
19944
20224
|
if (event.key === "Escape") {
|
|
19945
20225
|
closeExportPreviewMenu();
|
|
19946
20226
|
closePreviewLinkMenu();
|
|
20227
|
+
setFooterModelMenuOpen(false);
|
|
19947
20228
|
}
|
|
19948
20229
|
});
|
|
19949
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
|
+
|
|
19950
20260
|
saveAsBtn.addEventListener("click", () => {
|
|
19951
20261
|
const content = sourceTextEl.value;
|
|
19952
20262
|
if (!content.trim()) {
|
|
@@ -20190,6 +20500,18 @@
|
|
|
20190
20500
|
syncActionButtons();
|
|
20191
20501
|
});
|
|
20192
20502
|
}
|
|
20503
|
+
if (completionModelSelect) {
|
|
20504
|
+
completionModelSelect.value = completionSuggestionModelValue;
|
|
20505
|
+
completionModelSelect.addEventListener("change", () => {
|
|
20506
|
+
setCompletionSuggestionModelValue(completionModelSelect.value || "current");
|
|
20507
|
+
syncActionButtons();
|
|
20508
|
+
});
|
|
20509
|
+
}
|
|
20510
|
+
if (completionSuggestionRegenerateBtn) {
|
|
20511
|
+
completionSuggestionRegenerateBtn.addEventListener("click", () => {
|
|
20512
|
+
requestCompletionSuggestion({ regenerate: true });
|
|
20513
|
+
});
|
|
20514
|
+
}
|
|
20193
20515
|
if (completionSuggestionInsertBtn) {
|
|
20194
20516
|
completionSuggestionInsertBtn.addEventListener("click", () => {
|
|
20195
20517
|
insertCompletionSuggestion();
|