pi-studio 0.9.15 → 0.9.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/client/studio-client.js +400 -38
- package/client/studio.css +125 -15
- package/index.ts +247 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.17] — 2026-05-25
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Escape now dismisses a visible editor completion suggestion before falling through to other Studio Escape actions.
|
|
11
|
+
- Added a compact **Source & context** dropdown beside the editor-mode selector, with editor-only suggestions by default and an optional editor-plus-latest-response context mode; in-flight suggestion requests can now be stopped from the **Suggest** button.
|
|
12
|
+
- Kept Zen mode focused by hiding **Suggest** with the other secondary editor utilities while still showing **Send to REPL** controls when the REPL pane is active.
|
|
13
|
+
|
|
14
|
+
## [0.9.16] — 2026-05-24
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Added a first-cut manual **Suggest completion** editor action beside Run/Queue that asks the active model for a short cursor/selection-aware continuation, previews it before insertion, supports Option/Alt+Tab where available plus Cmd/Ctrl+Shift+Space from the editor, and lets Tab insert a visible suggestion.
|
|
18
|
+
|
|
7
19
|
## [0.9.15] — 2026-05-23
|
|
8
20
|
|
|
9
21
|
### Added
|
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
|
|
|
21
21
|
- Opens a two-pane browser workspace: **Editor** (left) + **Response/Working/Editor Preview** (right)
|
|
22
22
|
- Supports one canonical full Studio view per Pi session, plus additional editor-only companion views when you just want extra editing/preview surfaces; the editor toolbar can open a detached copy of the current editor text as a companion view
|
|
23
23
|
- Includes a global **Zen** mode for hiding secondary Studio chrome without changing the current left/right pane layout
|
|
24
|
-
- Runs editor text directly, asks for structured critique (auto/writing/code focus), or opens **Quiz me** for a Studio-native active-recall loop over the current editor text, selection, current file, folder, or repo, with optional focus guidance for shaping question selection
|
|
24
|
+
- Runs editor text directly, asks for structured critique (auto/writing/code focus), offers a manual **Suggest completion** action for short cursor-aware continuations (`Option/Alt+Tab` where available or `Cmd/Ctrl+Shift+Space` from the editor, `Tab` to insert a visible suggestion) with an optional editor-plus-latest-response context mode, or opens **Quiz me** for a Studio-native active-recall loop over the current editor text, selection, current file, folder, or repo, with optional focus guidance for shaping question selection
|
|
25
25
|
- Includes a live **Working** view for following current model/tool activity, with `All` / `Thinking` / `Tools` filters, image previews for image-producing tool outputs, plus **Load visible into editor** and **Copy visible** actions; when cycling response history, Working follows saved working details for the selected response when available
|
|
26
26
|
- Includes a right-pane **Files** view for browsing the current Pi session/resource directory, opening folders, loading text/code documents into the editor, previewing PDFs/images, copying paths, and revealing files in the file manager
|
|
27
27
|
- Includes an optional tmux-backed **REPL** view for Shell, Python, IPython, Julia, R, GHCi, and Clojure sessions, with Raw/Literate send modes, `Cmd/Ctrl+Shift+Enter` **Send to REPL**, session start/stop/interrupt controls, a compact refresh-persistent **Studio REPL Record** of user and Pi-sent code, a secondary raw tmux mirror, agent-facing `studio_repl_status` / `studio_repl_send` tools, and Markdown/PDF/HTML export
|
package/client/studio-client.js
CHANGED
|
@@ -114,6 +114,13 @@
|
|
|
114
114
|
const sendReplBtn = document.getElementById("sendReplBtn");
|
|
115
115
|
const replSendModeSelect = document.getElementById("replSendModeSelect");
|
|
116
116
|
const copyDraftBtn = document.getElementById("copyDraftBtn");
|
|
117
|
+
const suggestCompletionBtn = document.getElementById("suggestCompletionBtn");
|
|
118
|
+
const suggestCompletionOptionsBtn = document.getElementById("suggestCompletionOptionsBtn");
|
|
119
|
+
const completionContextSelect = document.getElementById("completionContextSelect");
|
|
120
|
+
const completionSuggestionPanelEl = document.getElementById("completionSuggestionPanel");
|
|
121
|
+
const completionSuggestionTextEl = document.getElementById("completionSuggestionText");
|
|
122
|
+
const completionSuggestionInsertBtn = document.getElementById("completionSuggestionInsertBtn");
|
|
123
|
+
const completionSuggestionDismissBtn = document.getElementById("completionSuggestionDismissBtn");
|
|
117
124
|
const saveAnnotatedBtn = document.getElementById("saveAnnotatedBtn");
|
|
118
125
|
const stripAnnotationsBtn = document.getElementById("stripAnnotationsBtn");
|
|
119
126
|
const highlightSelect = document.getElementById("highlightSelect");
|
|
@@ -251,6 +258,8 @@
|
|
|
251
258
|
const HTML_ARTIFACT_RESOURCE_FETCH_TIMEOUT_MS = 30_000;
|
|
252
259
|
const EDITOR_TAB_TEXT = " ";
|
|
253
260
|
const QUIZ_DEFAULT_COUNT = 5;
|
|
261
|
+
const COMPLETION_CONTEXT_STORAGE_KEY = "piStudio.completionContextMode";
|
|
262
|
+
const COMPLETION_CONTEXT_MAX_CHARS = 12000;
|
|
254
263
|
const QUIZ_SCOPES = ["editor", "selection", "file", "folder", "repo"];
|
|
255
264
|
const QUIZ_ANGLES = ["general", "scientist", "mathematician", "statistician", "developer", "reviewer"];
|
|
256
265
|
const QUIZ_THINKING_LEVELS = ["off", "minimal", "low", "medium", "high"];
|
|
@@ -1943,6 +1952,12 @@
|
|
|
1943
1952
|
let editorHighlightEnabled = false;
|
|
1944
1953
|
let editorLanguage = "markdown";
|
|
1945
1954
|
let responseHighlightEnabled = false;
|
|
1955
|
+
let completionSuggestionState = null;
|
|
1956
|
+
let completionSuggestionContextMode = readCompletionSuggestionContextMode();
|
|
1957
|
+
let completionSuggestionInFlight = false;
|
|
1958
|
+
let completionSuggestionRequestId = null;
|
|
1959
|
+
let completionSuggestionPendingSnapshot = null;
|
|
1960
|
+
let completionSuggestionRefocusEditorOnResult = false;
|
|
1946
1961
|
let editorHighlightRenderRaf = null;
|
|
1947
1962
|
let lineNumbersEnabled = false;
|
|
1948
1963
|
let lineNumbersRenderRaf = null;
|
|
@@ -2329,6 +2344,34 @@
|
|
|
2329
2344
|
appendStudioUiRefreshMenuSection(reviewMenu.menu, "Setting", [lensSelect]);
|
|
2330
2345
|
}
|
|
2331
2346
|
|
|
2347
|
+
let contextMenu = null;
|
|
2348
|
+
if (suggestCompletionOptionsBtn) {
|
|
2349
|
+
suggestCompletionOptionsBtn.hidden = false;
|
|
2350
|
+
if (completionContextSelect) completionContextSelect.hidden = true;
|
|
2351
|
+
contextMenu = makeStudioUiRefreshMenu(suggestCompletionOptionsBtn, "context", "studio-refresh-context-anchor");
|
|
2352
|
+
if (sourceBadgeEl) appendStudioUiRefreshMenuSection(contextMenu.menu, "Document", [sourceBadgeEl]);
|
|
2353
|
+
appendStudioUiRefreshMenuSection(contextMenu.menu, "Working directory", [resourceDirBtn, resourceDirLabel, resourceDirInputWrap]);
|
|
2354
|
+
const cursorContextBtn = makeStudioUiRefreshElement("button", "completion-context-option", "Editor only");
|
|
2355
|
+
cursorContextBtn.type = "button";
|
|
2356
|
+
cursorContextBtn.setAttribute("data-completion-context-mode", "cursor");
|
|
2357
|
+
const sessionContextBtn = makeStudioUiRefreshElement("button", "completion-context-option", "Editor + latest response");
|
|
2358
|
+
sessionContextBtn.type = "button";
|
|
2359
|
+
sessionContextBtn.setAttribute("data-completion-context-mode", "session");
|
|
2360
|
+
[cursorContextBtn, sessionContextBtn].forEach((button) => {
|
|
2361
|
+
button.addEventListener("click", (event) => {
|
|
2362
|
+
event.preventDefault();
|
|
2363
|
+
event.stopPropagation();
|
|
2364
|
+
setCompletionSuggestionContextMode(button.getAttribute("data-completion-context-mode") === "session" ? "session" : "cursor");
|
|
2365
|
+
syncActionButtons();
|
|
2366
|
+
});
|
|
2367
|
+
});
|
|
2368
|
+
appendStudioUiRefreshMenuSection(contextMenu.menu, "Suggestions", [cursorContextBtn, sessionContextBtn]);
|
|
2369
|
+
if (syncBadgeEl) {
|
|
2370
|
+
syncBadgeEl.hidden = false;
|
|
2371
|
+
appendStudioUiRefreshMenuSection(contextMenu.menu, "Status", [syncBadgeEl]);
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
|
|
2332
2375
|
const headerTopEl = makeStudioUiRefreshElement("div", "studio-refresh-header-top");
|
|
2333
2376
|
const titleGroupEl = makeStudioUiRefreshElement("div", "studio-refresh-title-group");
|
|
2334
2377
|
if (leftFocusBtn) {
|
|
@@ -2341,6 +2384,10 @@
|
|
|
2341
2384
|
} else if (editorViewSelect) {
|
|
2342
2385
|
titleGroupEl.appendChild(editorViewSelect);
|
|
2343
2386
|
}
|
|
2387
|
+
if (contextMenu) {
|
|
2388
|
+
titleGroupEl.appendChild(makeStudioUiRefreshSeparator());
|
|
2389
|
+
titleGroupEl.appendChild(contextMenu.anchor);
|
|
2390
|
+
}
|
|
2344
2391
|
headerTopEl.appendChild(titleGroupEl);
|
|
2345
2392
|
const headerToolsEl = makeStudioUiRefreshElement("div", "studio-refresh-pane-tools");
|
|
2346
2393
|
if (reviewNotesBtn) headerToolsEl.appendChild(reviewNotesBtn);
|
|
@@ -2349,18 +2396,7 @@
|
|
|
2349
2396
|
if (reviewMenu) headerToolsEl.appendChild(reviewMenu.anchor);
|
|
2350
2397
|
headerTopEl.appendChild(headerToolsEl);
|
|
2351
2398
|
|
|
2352
|
-
|
|
2353
|
-
const utilityLeftEl = makeStudioUiRefreshElement("div", "studio-refresh-utility-left");
|
|
2354
|
-
if (sourceBadgeEl) utilityLeftEl.appendChild(sourceBadgeEl);
|
|
2355
|
-
if (sourceBadgeEl && (resourceDirBtn || resourceDirLabel || resourceDirInputWrap || syncBadgeEl)) {
|
|
2356
|
-
utilityLeftEl.appendChild(makeStudioUiRefreshSeparator());
|
|
2357
|
-
}
|
|
2358
|
-
if (resourceDirBtn) utilityLeftEl.appendChild(resourceDirBtn);
|
|
2359
|
-
if (resourceDirLabel) utilityLeftEl.appendChild(resourceDirLabel);
|
|
2360
|
-
if (resourceDirInputWrap) utilityLeftEl.appendChild(resourceDirInputWrap);
|
|
2361
|
-
if (syncBadgeEl) utilityLeftEl.appendChild(syncBadgeEl);
|
|
2362
|
-
headerUtilityEl.appendChild(utilityLeftEl);
|
|
2363
|
-
leftHeaderEl.replaceChildren(headerTopEl, headerUtilityEl);
|
|
2399
|
+
leftHeaderEl.replaceChildren(headerTopEl);
|
|
2364
2400
|
|
|
2365
2401
|
const rightHeaderEl = document.getElementById("rightSectionHeader");
|
|
2366
2402
|
if (rightHeaderEl && rightViewSelect) {
|
|
@@ -2392,17 +2428,18 @@
|
|
|
2392
2428
|
const actionLineOneEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
|
|
2393
2429
|
if (!isEditorOnlyMode && sendRunBtn) actionLineOneEl.appendChild(sendRunBtn);
|
|
2394
2430
|
if (!isEditorOnlyMode && queueSteerBtn) actionLineOneEl.appendChild(queueSteerBtn);
|
|
2431
|
+
const actionLineTwoEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line studio-refresh-utility-action-line");
|
|
2432
|
+
actionLineTwoEl.appendChild(copyDraftBtn);
|
|
2433
|
+
if (suggestCompletionBtn) actionLineTwoEl.appendChild(suggestCompletionBtn);
|
|
2434
|
+
if (openCompanionBtn) actionLineTwoEl.appendChild(openCompanionBtn);
|
|
2435
|
+
if (!isEditorOnlyMode && sendEditorBtn) actionLineTwoEl.appendChild(sendEditorBtn);
|
|
2395
2436
|
const replActionLineEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line repl-action-line");
|
|
2396
2437
|
replActionLineEl.hidden = true;
|
|
2397
2438
|
if (!isEditorOnlyMode && sendReplBtn) replActionLineEl.appendChild(sendReplBtn);
|
|
2398
2439
|
if (!isEditorOnlyMode && replSendModeSelect) replActionLineEl.appendChild(replSendModeSelect);
|
|
2399
|
-
const actionLineTwoEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
|
|
2400
|
-
actionLineTwoEl.appendChild(copyDraftBtn);
|
|
2401
|
-
if (openCompanionBtn) actionLineTwoEl.appendChild(openCompanionBtn);
|
|
2402
|
-
if (!isEditorOnlyMode && sendEditorBtn) actionLineTwoEl.appendChild(sendEditorBtn);
|
|
2403
2440
|
if (actionLineOneEl.childNodes.length > 0) actionsEl.appendChild(actionLineOneEl);
|
|
2404
|
-
if (replActionLineEl.childNodes.length > 0) actionsEl.appendChild(replActionLineEl);
|
|
2405
2441
|
actionsEl.appendChild(actionLineTwoEl);
|
|
2442
|
+
if (replActionLineEl.childNodes.length > 0) actionsEl.appendChild(replActionLineEl);
|
|
2406
2443
|
|
|
2407
2444
|
const stateEl = makeStudioUiRefreshElement("div", "studio-refresh-toolbar-state");
|
|
2408
2445
|
const annotationsButton = makeStudioUiRefreshElement("button", "", "Annotations");
|
|
@@ -2424,7 +2461,9 @@
|
|
|
2424
2461
|
annotationsButton,
|
|
2425
2462
|
viewButton,
|
|
2426
2463
|
reviewButton: reviewMenu ? reviewMenu.button : null,
|
|
2427
|
-
menus: [annotationsMenu, viewMenu]
|
|
2464
|
+
menus: [annotationsMenu, viewMenu]
|
|
2465
|
+
.concat(contextMenu ? [contextMenu] : [])
|
|
2466
|
+
.concat(reviewMenu ? [reviewMenu] : []),
|
|
2428
2467
|
};
|
|
2429
2468
|
|
|
2430
2469
|
document.addEventListener("click", (event) => {
|
|
@@ -3680,6 +3719,16 @@
|
|
|
3680
3719
|
return;
|
|
3681
3720
|
}
|
|
3682
3721
|
|
|
3722
|
+
if (plainEscape && completionSuggestionState) {
|
|
3723
|
+
event.preventDefault();
|
|
3724
|
+
hideCompletionSuggestion();
|
|
3725
|
+
focusSourceTextNoScroll();
|
|
3726
|
+
setStatus("Dismissed completion suggestion.");
|
|
3727
|
+
return;
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
if (handleCompletionSuggestionAcceptKey(event)) return;
|
|
3731
|
+
|
|
3683
3732
|
if ((key === "?" || (key === "/" && event.shiftKey)) && !event.metaKey && !event.ctrlKey && !event.altKey && !isTextEntryShortcutTarget(event.target)) {
|
|
3684
3733
|
event.preventDefault();
|
|
3685
3734
|
toggleShortcuts();
|
|
@@ -4231,20 +4280,15 @@
|
|
|
4231
4280
|
|
|
4232
4281
|
if (isEditorOnlyMode) {
|
|
4233
4282
|
syncBadgeEl.hidden = true;
|
|
4234
|
-
syncBadgeEl.
|
|
4235
|
-
|
|
4236
|
-
}
|
|
4237
|
-
|
|
4238
|
-
if (rightView === "trace") {
|
|
4239
|
-
syncBadgeEl.hidden = true;
|
|
4240
|
-
syncBadgeEl.classList.remove("sync");
|
|
4283
|
+
syncBadgeEl.textContent = "Editor-only tab";
|
|
4284
|
+
syncBadgeEl.classList.remove("sync", "out-of-sync");
|
|
4241
4285
|
return;
|
|
4242
4286
|
}
|
|
4243
4287
|
|
|
4244
4288
|
if (!latestResponseHasContent) {
|
|
4245
|
-
syncBadgeEl.hidden =
|
|
4246
|
-
syncBadgeEl.textContent = "
|
|
4247
|
-
syncBadgeEl.classList.remove("sync");
|
|
4289
|
+
syncBadgeEl.hidden = false;
|
|
4290
|
+
syncBadgeEl.textContent = "No latest response";
|
|
4291
|
+
syncBadgeEl.classList.remove("sync", "out-of-sync");
|
|
4248
4292
|
return;
|
|
4249
4293
|
}
|
|
4250
4294
|
|
|
@@ -4252,15 +4296,10 @@
|
|
|
4252
4296
|
? normalizedEditorText
|
|
4253
4297
|
: normalizeForCompare(sourceTextEl.value);
|
|
4254
4298
|
const inSync = normalizedEditor === latestResponseNormalized;
|
|
4255
|
-
syncBadgeEl.hidden =
|
|
4256
|
-
syncBadgeEl.textContent = "In sync with response";
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
syncBadgeEl.classList.add("sync");
|
|
4260
|
-
return;
|
|
4261
|
-
}
|
|
4262
|
-
|
|
4263
|
-
syncBadgeEl.classList.remove("sync");
|
|
4299
|
+
syncBadgeEl.hidden = false;
|
|
4300
|
+
syncBadgeEl.textContent = inSync ? "In sync with response" : "Editor differs from latest response";
|
|
4301
|
+
syncBadgeEl.classList.toggle("sync", inSync);
|
|
4302
|
+
syncBadgeEl.classList.toggle("out-of-sync", !inSync);
|
|
4264
4303
|
}
|
|
4265
4304
|
|
|
4266
4305
|
function buildPlainMarkdownHtml(markdown, options) {
|
|
@@ -8653,6 +8692,15 @@
|
|
|
8653
8692
|
if (loadGitDiffBtn) loadGitDiffBtn.disabled = uiBusy;
|
|
8654
8693
|
syncRunAndCritiqueButtons();
|
|
8655
8694
|
copyDraftBtn.disabled = uiBusy;
|
|
8695
|
+
if (suggestCompletionBtn) {
|
|
8696
|
+
suggestCompletionBtn.disabled = wsState !== "Ready" || (!completionSuggestionInFlight && (uiBusy || !String(sourceTextEl.value || "").trim()));
|
|
8697
|
+
suggestCompletionBtn.textContent = completionSuggestionInFlight ? "Stop" : "Suggest";
|
|
8698
|
+
suggestCompletionBtn.title = completionSuggestionInFlight
|
|
8699
|
+
? "Stop the current suggestion request."
|
|
8700
|
+
: "Ask the current model for a short completion at the editor cursor. Shortcut: Option/Alt+Tab where available, or Cmd/Ctrl+Shift+Space from the editor.";
|
|
8701
|
+
}
|
|
8702
|
+
if (suggestCompletionOptionsBtn) suggestCompletionOptionsBtn.disabled = uiBusy || completionSuggestionInFlight;
|
|
8703
|
+
syncCompletionSuggestionContextUi();
|
|
8656
8704
|
if (openCompanionBtn) openCompanionBtn.disabled = uiBusy || wsState !== "Ready";
|
|
8657
8705
|
if (highlightSelect) highlightSelect.disabled = uiBusy;
|
|
8658
8706
|
if (lineNumbersSelect) lineNumbersSelect.disabled = uiBusy;
|
|
@@ -8986,6 +9034,282 @@
|
|
|
8986
9034
|
}
|
|
8987
9035
|
}
|
|
8988
9036
|
|
|
9037
|
+
function readCompletionSuggestionContextMode() {
|
|
9038
|
+
try {
|
|
9039
|
+
const stored = window.localStorage ? String(window.localStorage.getItem(COMPLETION_CONTEXT_STORAGE_KEY) || "") : "";
|
|
9040
|
+
return stored === "session" ? "session" : "cursor";
|
|
9041
|
+
} catch {
|
|
9042
|
+
return "cursor";
|
|
9043
|
+
}
|
|
9044
|
+
}
|
|
9045
|
+
|
|
9046
|
+
function setCompletionSuggestionContextMode(mode) {
|
|
9047
|
+
completionSuggestionContextMode = mode === "session" ? "session" : "cursor";
|
|
9048
|
+
if (completionContextSelect) completionContextSelect.value = completionSuggestionContextMode;
|
|
9049
|
+
try {
|
|
9050
|
+
if (window.localStorage) window.localStorage.setItem(COMPLETION_CONTEXT_STORAGE_KEY, completionSuggestionContextMode);
|
|
9051
|
+
} catch {}
|
|
9052
|
+
setStatus(completionSuggestionContextMode === "session"
|
|
9053
|
+
? "Suggestions will include the latest response as context."
|
|
9054
|
+
: "Suggestions will use cursor-local editor context only.");
|
|
9055
|
+
}
|
|
9056
|
+
|
|
9057
|
+
function syncCompletionSuggestionContextUi() {
|
|
9058
|
+
if (completionContextSelect) completionContextSelect.value = completionSuggestionContextMode;
|
|
9059
|
+
if (suggestCompletionOptionsBtn) {
|
|
9060
|
+
suggestCompletionOptionsBtn.textContent = "Source & context";
|
|
9061
|
+
suggestCompletionOptionsBtn.title = completionSuggestionContextMode === "session"
|
|
9062
|
+
? "Document source, working directory, status, and suggestion context. Suggestions include editor plus latest response."
|
|
9063
|
+
: "Document source, working directory, status, and suggestion context. Suggestions use editor-only context.";
|
|
9064
|
+
suggestCompletionOptionsBtn.setAttribute("aria-label", suggestCompletionOptionsBtn.title);
|
|
9065
|
+
}
|
|
9066
|
+
document.querySelectorAll("[data-completion-context-mode]").forEach((button) => {
|
|
9067
|
+
if (!(button instanceof HTMLElement)) return;
|
|
9068
|
+
const mode = button.getAttribute("data-completion-context-mode") === "session" ? "session" : "cursor";
|
|
9069
|
+
const selected = mode === completionSuggestionContextMode;
|
|
9070
|
+
button.classList.toggle("is-selected", selected);
|
|
9071
|
+
button.setAttribute("aria-pressed", selected ? "true" : "false");
|
|
9072
|
+
button.textContent = (selected ? "✓ " : " ") + (mode === "session" ? "Editor + latest response" : "Editor only");
|
|
9073
|
+
});
|
|
9074
|
+
}
|
|
9075
|
+
|
|
9076
|
+
function trimCompletionContextText(text) {
|
|
9077
|
+
const value = String(text || "").trim();
|
|
9078
|
+
if (value.length <= COMPLETION_CONTEXT_MAX_CHARS) return value;
|
|
9079
|
+
return value.slice(value.length - COMPLETION_CONTEXT_MAX_CHARS);
|
|
9080
|
+
}
|
|
9081
|
+
|
|
9082
|
+
function getCompletionSuggestionContextText() {
|
|
9083
|
+
if (completionSuggestionContextMode !== "session") return "";
|
|
9084
|
+
const selected = getSelectedHistoryItem ? getSelectedHistoryItem() : null;
|
|
9085
|
+
const responseText = selected && typeof selected.markdown === "string" && selected.markdown.trim()
|
|
9086
|
+
? selected.markdown
|
|
9087
|
+
: latestResponseMarkdown;
|
|
9088
|
+
const parts = [];
|
|
9089
|
+
if (selected && typeof selected.promptTriggerText === "string" && selected.promptTriggerText.trim()) {
|
|
9090
|
+
parts.push("Latest request/steering:\n" + trimCompletionContextText(selected.promptTriggerText));
|
|
9091
|
+
} else if (selected && typeof selected.prompt === "string" && selected.prompt.trim()) {
|
|
9092
|
+
parts.push("Latest prompt:\n" + trimCompletionContextText(selected.prompt));
|
|
9093
|
+
}
|
|
9094
|
+
if (String(responseText || "").trim()) {
|
|
9095
|
+
parts.push("Latest response:\n" + trimCompletionContextText(responseText));
|
|
9096
|
+
}
|
|
9097
|
+
return trimCompletionContextText(parts.join("\n\n---\n\n"));
|
|
9098
|
+
}
|
|
9099
|
+
|
|
9100
|
+
function hideCompletionSuggestion() {
|
|
9101
|
+
completionSuggestionState = null;
|
|
9102
|
+
if (completionSuggestionTextEl) completionSuggestionTextEl.textContent = "";
|
|
9103
|
+
if (completionSuggestionPanelEl) completionSuggestionPanelEl.hidden = true;
|
|
9104
|
+
}
|
|
9105
|
+
|
|
9106
|
+
function showCompletionSuggestion(state) {
|
|
9107
|
+
completionSuggestionState = state;
|
|
9108
|
+
if (completionSuggestionTextEl) completionSuggestionTextEl.textContent = state && state.suggestion ? state.suggestion : "";
|
|
9109
|
+
if (completionSuggestionPanelEl) completionSuggestionPanelEl.hidden = false;
|
|
9110
|
+
}
|
|
9111
|
+
|
|
9112
|
+
function focusSourceTextNoScroll() {
|
|
9113
|
+
if (!sourceTextEl || typeof sourceTextEl.focus !== "function") return;
|
|
9114
|
+
try {
|
|
9115
|
+
sourceTextEl.focus({ preventScroll: true });
|
|
9116
|
+
} catch {
|
|
9117
|
+
try { sourceTextEl.focus(); } catch {}
|
|
9118
|
+
}
|
|
9119
|
+
}
|
|
9120
|
+
|
|
9121
|
+
function focusSourceEditorForCompletion() {
|
|
9122
|
+
const snapshot = snapshotStudioScrollablePositions();
|
|
9123
|
+
if (editorView !== "markdown") {
|
|
9124
|
+
setEditorView("markdown");
|
|
9125
|
+
scheduleStudioScrollablePositionRestore(snapshot);
|
|
9126
|
+
}
|
|
9127
|
+
window.setTimeout(focusSourceTextNoScroll, 0);
|
|
9128
|
+
}
|
|
9129
|
+
|
|
9130
|
+
function isCompletionSuggestionRequestShortcut(event) {
|
|
9131
|
+
if (!event) return false;
|
|
9132
|
+
const key = typeof event.key === "string" ? event.key : "";
|
|
9133
|
+
const code = typeof event.code === "string" ? event.code : "";
|
|
9134
|
+
const commandSpace = (event.metaKey || event.ctrlKey)
|
|
9135
|
+
&& event.shiftKey
|
|
9136
|
+
&& !event.altKey
|
|
9137
|
+
&& (code === "Space" || key === " " || key === "Spacebar");
|
|
9138
|
+
const optionTab = event.altKey
|
|
9139
|
+
&& !event.metaKey
|
|
9140
|
+
&& !event.ctrlKey
|
|
9141
|
+
&& !event.shiftKey
|
|
9142
|
+
&& key === "Tab";
|
|
9143
|
+
return commandSpace || optionTab;
|
|
9144
|
+
}
|
|
9145
|
+
|
|
9146
|
+
function handleCompletionSuggestionAcceptKey(event) {
|
|
9147
|
+
if (!event || !completionSuggestionState) return false;
|
|
9148
|
+
if (event.key !== "Tab" || event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return false;
|
|
9149
|
+
const target = event.target;
|
|
9150
|
+
const focusIsUnclaimed = target === document.body || target === document.documentElement;
|
|
9151
|
+
const targetCanAccept = focusIsUnclaimed
|
|
9152
|
+
|| target === sourceTextEl
|
|
9153
|
+
|| target === suggestCompletionBtn
|
|
9154
|
+
|| Boolean(completionSuggestionPanelEl && target instanceof Element && completionSuggestionPanelEl.contains(target));
|
|
9155
|
+
if (!targetCanAccept) return false;
|
|
9156
|
+
event.preventDefault();
|
|
9157
|
+
insertCompletionSuggestion();
|
|
9158
|
+
return true;
|
|
9159
|
+
}
|
|
9160
|
+
|
|
9161
|
+
function shouldRefocusEditorForCompletionRequest() {
|
|
9162
|
+
const activeEl = document.activeElement;
|
|
9163
|
+
return activeEl === sourceTextEl
|
|
9164
|
+
|| activeEl === suggestCompletionBtn
|
|
9165
|
+
|| activeEl === document.body
|
|
9166
|
+
|| activeEl === document.documentElement
|
|
9167
|
+
|| Boolean(completionSuggestionPanelEl && activeEl instanceof Element && completionSuggestionPanelEl.contains(activeEl));
|
|
9168
|
+
}
|
|
9169
|
+
|
|
9170
|
+
function cancelCompletionSuggestion() {
|
|
9171
|
+
if (!completionSuggestionInFlight || !completionSuggestionRequestId) {
|
|
9172
|
+
setStatus("No suggestion request is running.", "warning");
|
|
9173
|
+
return;
|
|
9174
|
+
}
|
|
9175
|
+
setStatus("Stopping suggestion…", "warning");
|
|
9176
|
+
const sent = sendMessage({
|
|
9177
|
+
type: "completion_suggestion_cancel_request",
|
|
9178
|
+
requestId: completionSuggestionRequestId,
|
|
9179
|
+
});
|
|
9180
|
+
if (!sent) {
|
|
9181
|
+
completionSuggestionInFlight = false;
|
|
9182
|
+
completionSuggestionRequestId = null;
|
|
9183
|
+
completionSuggestionPendingSnapshot = null;
|
|
9184
|
+
completionSuggestionRefocusEditorOnResult = false;
|
|
9185
|
+
syncActionButtons();
|
|
9186
|
+
}
|
|
9187
|
+
}
|
|
9188
|
+
|
|
9189
|
+
function requestCompletionSuggestion() {
|
|
9190
|
+
if (isEditorOnlyMode && !sourceTextEl) return;
|
|
9191
|
+
if (completionSuggestionInFlight) {
|
|
9192
|
+
cancelCompletionSuggestion();
|
|
9193
|
+
return;
|
|
9194
|
+
}
|
|
9195
|
+
const text = String(sourceTextEl.value || "");
|
|
9196
|
+
if (!text.trim()) {
|
|
9197
|
+
setStatus("Editor is empty.", "warning");
|
|
9198
|
+
return;
|
|
9199
|
+
}
|
|
9200
|
+
const selectionStart = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : text.length;
|
|
9201
|
+
const selectionEnd = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : selectionStart;
|
|
9202
|
+
const contextText = getCompletionSuggestionContextText();
|
|
9203
|
+
const requestId = makeRequestId();
|
|
9204
|
+
completionSuggestionInFlight = true;
|
|
9205
|
+
completionSuggestionRequestId = requestId;
|
|
9206
|
+
completionSuggestionPendingSnapshot = { text, selectionStart, selectionEnd };
|
|
9207
|
+
completionSuggestionRefocusEditorOnResult = shouldRefocusEditorForCompletionRequest();
|
|
9208
|
+
hideCompletionSuggestion();
|
|
9209
|
+
syncActionButtons();
|
|
9210
|
+
setStatus("Generating completion suggestion…", "warning");
|
|
9211
|
+
const sent = sendMessage({
|
|
9212
|
+
type: "completion_suggestion_request",
|
|
9213
|
+
requestId,
|
|
9214
|
+
text,
|
|
9215
|
+
selectionStart,
|
|
9216
|
+
selectionEnd,
|
|
9217
|
+
language: editorLanguage || "",
|
|
9218
|
+
label: sourceState && sourceState.label ? sourceState.label : "Studio editor",
|
|
9219
|
+
path: sourceState && sourceState.path ? sourceState.path : undefined,
|
|
9220
|
+
contextMode: completionSuggestionContextMode,
|
|
9221
|
+
contextText: contextText || undefined,
|
|
9222
|
+
});
|
|
9223
|
+
if (!sent) {
|
|
9224
|
+
completionSuggestionInFlight = false;
|
|
9225
|
+
completionSuggestionRequestId = null;
|
|
9226
|
+
completionSuggestionPendingSnapshot = null;
|
|
9227
|
+
completionSuggestionRefocusEditorOnResult = false;
|
|
9228
|
+
syncActionButtons();
|
|
9229
|
+
}
|
|
9230
|
+
}
|
|
9231
|
+
|
|
9232
|
+
function insertCompletionSuggestion() {
|
|
9233
|
+
const state = completionSuggestionState;
|
|
9234
|
+
if (!state || typeof state.suggestion !== "string") {
|
|
9235
|
+
setStatus("No suggestion to insert.", "warning");
|
|
9236
|
+
return;
|
|
9237
|
+
}
|
|
9238
|
+
const currentText = String(sourceTextEl.value || "");
|
|
9239
|
+
const useOriginalRange = currentText === state.baseText;
|
|
9240
|
+
const start = useOriginalRange
|
|
9241
|
+
? Math.max(0, Math.min(state.selectionStart, currentText.length))
|
|
9242
|
+
: (typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : currentText.length);
|
|
9243
|
+
const end = useOriginalRange
|
|
9244
|
+
? Math.max(start, Math.min(state.selectionEnd, currentText.length))
|
|
9245
|
+
: (typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start);
|
|
9246
|
+
const nextText = currentText.slice(0, start) + state.suggestion + currentText.slice(end);
|
|
9247
|
+
const caret = start + state.suggestion.length;
|
|
9248
|
+
applySourceTextEdit(nextText, caret, caret);
|
|
9249
|
+
hideCompletionSuggestion();
|
|
9250
|
+
focusSourceTextNoScroll();
|
|
9251
|
+
setStatus("Inserted completion suggestion.", "success");
|
|
9252
|
+
}
|
|
9253
|
+
|
|
9254
|
+
function handleCompletionSuggestionServerMessage(message) {
|
|
9255
|
+
if (!message || typeof message !== "object") return false;
|
|
9256
|
+
if (
|
|
9257
|
+
message.type !== "completion_suggestion_progress"
|
|
9258
|
+
&& message.type !== "completion_suggestion_result"
|
|
9259
|
+
&& message.type !== "completion_suggestion_error"
|
|
9260
|
+
) return false;
|
|
9261
|
+
if (typeof message.requestId === "string" && completionSuggestionRequestId && message.requestId !== completionSuggestionRequestId) {
|
|
9262
|
+
return true;
|
|
9263
|
+
}
|
|
9264
|
+
if (message.type === "completion_suggestion_progress") {
|
|
9265
|
+
setStatus(typeof message.message === "string" ? message.message : "Generating suggestion…", "warning");
|
|
9266
|
+
return true;
|
|
9267
|
+
}
|
|
9268
|
+
const pendingSnapshot = completionSuggestionPendingSnapshot;
|
|
9269
|
+
const shouldRefocusEditor = completionSuggestionRefocusEditorOnResult;
|
|
9270
|
+
completionSuggestionInFlight = false;
|
|
9271
|
+
completionSuggestionRequestId = null;
|
|
9272
|
+
completionSuggestionPendingSnapshot = null;
|
|
9273
|
+
completionSuggestionRefocusEditorOnResult = false;
|
|
9274
|
+
syncActionButtons();
|
|
9275
|
+
if (message.type === "completion_suggestion_error") {
|
|
9276
|
+
setStatus(typeof message.message === "string" ? message.message : "Suggestion failed.", "warning");
|
|
9277
|
+
return true;
|
|
9278
|
+
}
|
|
9279
|
+
const suggestion = typeof message.suggestion === "string" ? message.suggestion : "";
|
|
9280
|
+
if (!suggestion.trim()) {
|
|
9281
|
+
setStatus("Model returned an empty suggestion.", "warning");
|
|
9282
|
+
return true;
|
|
9283
|
+
}
|
|
9284
|
+
const text = String(sourceTextEl.value || "");
|
|
9285
|
+
if (pendingSnapshot && text !== pendingSnapshot.text) {
|
|
9286
|
+
setStatus("Editor changed while the suggestion was generating. Please request a fresh suggestion.", "warning");
|
|
9287
|
+
return true;
|
|
9288
|
+
}
|
|
9289
|
+
const baseText = pendingSnapshot ? pendingSnapshot.text : text;
|
|
9290
|
+
const start = Math.max(0, Math.min(pendingSnapshot ? pendingSnapshot.selectionStart : (Number(message.selectionStart) || 0), baseText.length));
|
|
9291
|
+
const end = Math.max(start, Math.min(pendingSnapshot ? pendingSnapshot.selectionEnd : (Number(message.selectionEnd) || start), baseText.length));
|
|
9292
|
+
showCompletionSuggestion({
|
|
9293
|
+
suggestion,
|
|
9294
|
+
baseText,
|
|
9295
|
+
selectionStart: start,
|
|
9296
|
+
selectionEnd: end,
|
|
9297
|
+
});
|
|
9298
|
+
const activeEl = document.activeElement;
|
|
9299
|
+
if (
|
|
9300
|
+
shouldRefocusEditor
|
|
9301
|
+
|| activeEl === sourceTextEl
|
|
9302
|
+
|| activeEl === suggestCompletionBtn
|
|
9303
|
+
|| activeEl === document.body
|
|
9304
|
+
|| activeEl === document.documentElement
|
|
9305
|
+
|| Boolean(completionSuggestionPanelEl && activeEl instanceof Element && completionSuggestionPanelEl.contains(activeEl))
|
|
9306
|
+
) {
|
|
9307
|
+
focusSourceEditorForCompletion();
|
|
9308
|
+
}
|
|
9309
|
+
setStatus("Suggestion ready. Press Tab to insert it, or use the Insert suggestion button.", "success");
|
|
9310
|
+
return true;
|
|
9311
|
+
}
|
|
9312
|
+
|
|
8989
9313
|
function getSourceTextLineEditBounds(text, selectionStart, selectionEnd) {
|
|
8990
9314
|
const source = String(text || "");
|
|
8991
9315
|
const safeStart = Math.max(0, Math.min(Math.floor(Number(selectionStart) || 0), source.length));
|
|
@@ -9065,7 +9389,14 @@
|
|
|
9065
9389
|
}
|
|
9066
9390
|
|
|
9067
9391
|
function handleSourceTextTabKey(event) {
|
|
9068
|
-
if (!event
|
|
9392
|
+
if (!event) return;
|
|
9393
|
+
if (isCompletionSuggestionRequestShortcut(event)) {
|
|
9394
|
+
event.preventDefault();
|
|
9395
|
+
requestCompletionSuggestion();
|
|
9396
|
+
return;
|
|
9397
|
+
}
|
|
9398
|
+
if (handleCompletionSuggestionAcceptKey(event)) return;
|
|
9399
|
+
if (event.key !== "Tab" || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
9069
9400
|
event.preventDefault();
|
|
9070
9401
|
if (event.shiftKey) {
|
|
9071
9402
|
unindentSourceTextSelection();
|
|
@@ -16007,6 +16338,8 @@
|
|
|
16007
16338
|
updateFooterMeta();
|
|
16008
16339
|
}
|
|
16009
16340
|
|
|
16341
|
+
if (handleCompletionSuggestionServerMessage(message)) return;
|
|
16342
|
+
|
|
16010
16343
|
if (
|
|
16011
16344
|
message.type === "quiz_progress" ||
|
|
16012
16345
|
message.type === "quiz_generated" ||
|
|
@@ -17258,6 +17591,9 @@
|
|
|
17258
17591
|
sourceTextEl.addEventListener("keydown", handleSourceTextTabKey);
|
|
17259
17592
|
|
|
17260
17593
|
sourceTextEl.addEventListener("input", () => {
|
|
17594
|
+
if (completionSuggestionState && sourceTextEl.value !== completionSuggestionState.baseText) {
|
|
17595
|
+
hideCompletionSuggestion();
|
|
17596
|
+
}
|
|
17261
17597
|
if (activePreviewCommentSelection) {
|
|
17262
17598
|
clearPreviewCommentSelection();
|
|
17263
17599
|
}
|
|
@@ -17266,6 +17602,7 @@
|
|
|
17266
17602
|
scheduleEditorMetaUpdate();
|
|
17267
17603
|
updateEditorSelectionCommentUi();
|
|
17268
17604
|
updateOutlineUi();
|
|
17605
|
+
syncActionButtons();
|
|
17269
17606
|
if (isReviewNotesOpen() && reviewNotes.length > 0) {
|
|
17270
17607
|
renderReviewNotesList();
|
|
17271
17608
|
updateReviewNotesUi();
|
|
@@ -17721,6 +18058,31 @@
|
|
|
17721
18058
|
}
|
|
17722
18059
|
});
|
|
17723
18060
|
|
|
18061
|
+
if (suggestCompletionBtn) {
|
|
18062
|
+
suggestCompletionBtn.addEventListener("click", () => {
|
|
18063
|
+
requestCompletionSuggestion();
|
|
18064
|
+
});
|
|
18065
|
+
}
|
|
18066
|
+
if (completionContextSelect) {
|
|
18067
|
+
completionContextSelect.value = completionSuggestionContextMode;
|
|
18068
|
+
completionContextSelect.addEventListener("change", () => {
|
|
18069
|
+
setCompletionSuggestionContextMode(completionContextSelect.value);
|
|
18070
|
+
syncActionButtons();
|
|
18071
|
+
});
|
|
18072
|
+
}
|
|
18073
|
+
if (completionSuggestionInsertBtn) {
|
|
18074
|
+
completionSuggestionInsertBtn.addEventListener("click", () => {
|
|
18075
|
+
insertCompletionSuggestion();
|
|
18076
|
+
});
|
|
18077
|
+
}
|
|
18078
|
+
if (completionSuggestionDismissBtn) {
|
|
18079
|
+
completionSuggestionDismissBtn.addEventListener("click", () => {
|
|
18080
|
+
hideCompletionSuggestion();
|
|
18081
|
+
focusSourceTextNoScroll();
|
|
18082
|
+
setStatus("Dismissed completion suggestion.");
|
|
18083
|
+
});
|
|
18084
|
+
}
|
|
18085
|
+
|
|
17724
18086
|
if (reviewNotesBtn) {
|
|
17725
18087
|
reviewNotesBtn.addEventListener("click", () => {
|
|
17726
18088
|
toggleReviewNotes();
|
package/client/studio.css
CHANGED
|
@@ -849,6 +849,63 @@
|
|
|
849
849
|
-webkit-text-fill-color: transparent;
|
|
850
850
|
}
|
|
851
851
|
|
|
852
|
+
.completion-suggestion-panel {
|
|
853
|
+
border: 1px solid var(--control-border);
|
|
854
|
+
border-radius: 10px;
|
|
855
|
+
background: var(--panel-2);
|
|
856
|
+
box-shadow: var(--panel-shadow);
|
|
857
|
+
overflow: hidden;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
.completion-suggestion-panel[hidden] {
|
|
861
|
+
display: none !important;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
.completion-suggestion-header,
|
|
865
|
+
.completion-suggestion-actions {
|
|
866
|
+
display: flex;
|
|
867
|
+
align-items: center;
|
|
868
|
+
justify-content: space-between;
|
|
869
|
+
gap: 8px;
|
|
870
|
+
padding: 8px 10px;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
.completion-suggestion-header {
|
|
874
|
+
border-bottom: 1px solid var(--border-subtle);
|
|
875
|
+
color: var(--studio-info-text, var(--muted));
|
|
876
|
+
font-size: 12px;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
.completion-suggestion-header strong {
|
|
880
|
+
color: var(--text);
|
|
881
|
+
font-weight: 650;
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
.completion-suggestion-text {
|
|
885
|
+
margin: 0;
|
|
886
|
+
max-height: 180px;
|
|
887
|
+
overflow: auto;
|
|
888
|
+
padding: 10px;
|
|
889
|
+
white-space: pre-wrap;
|
|
890
|
+
word-break: break-word;
|
|
891
|
+
font-family: var(--font-mono);
|
|
892
|
+
font-size: var(--studio-editor-font-size);
|
|
893
|
+
line-height: 1.45;
|
|
894
|
+
color: var(--text);
|
|
895
|
+
background: var(--editor-bg);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
.completion-suggestion-actions {
|
|
899
|
+
justify-content: flex-end;
|
|
900
|
+
border-top: 1px solid var(--border-subtle);
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
.completion-suggestion-header button,
|
|
904
|
+
.completion-suggestion-actions button {
|
|
905
|
+
padding: 4px 8px;
|
|
906
|
+
font-size: 12px;
|
|
907
|
+
}
|
|
908
|
+
|
|
852
909
|
.editor-selection-actions {
|
|
853
910
|
position: absolute;
|
|
854
911
|
top: 12px;
|
|
@@ -3766,8 +3823,8 @@
|
|
|
3766
3823
|
|
|
3767
3824
|
.shortcuts-group dl > div {
|
|
3768
3825
|
display: grid;
|
|
3769
|
-
grid-template-columns: minmax(
|
|
3770
|
-
gap:
|
|
3826
|
+
grid-template-columns: minmax(260px, 260px) minmax(0, 1fr);
|
|
3827
|
+
gap: 24px;
|
|
3771
3828
|
align-items: baseline;
|
|
3772
3829
|
padding: 6px 10px;
|
|
3773
3830
|
border-top: 1px solid var(--border-subtle);
|
|
@@ -4188,13 +4245,18 @@
|
|
|
4188
4245
|
justify-content: stretch;
|
|
4189
4246
|
}
|
|
4190
4247
|
|
|
4191
|
-
body.studio-ui-refresh .studio-refresh-pane-tools
|
|
4192
|
-
body.studio-ui-refresh .studio-refresh-toolbar-state {
|
|
4248
|
+
body.studio-ui-refresh .studio-refresh-pane-tools {
|
|
4193
4249
|
justify-content: flex-start;
|
|
4194
4250
|
justify-items: start;
|
|
4195
4251
|
min-width: 0;
|
|
4196
4252
|
}
|
|
4197
4253
|
|
|
4254
|
+
body.studio-ui-refresh .studio-refresh-toolbar-state {
|
|
4255
|
+
justify-content: flex-end;
|
|
4256
|
+
justify-items: end;
|
|
4257
|
+
min-width: 0;
|
|
4258
|
+
}
|
|
4259
|
+
|
|
4198
4260
|
body.studio-ui-refresh .studio-refresh-pane-tools,
|
|
4199
4261
|
body.studio-ui-refresh .studio-refresh-title-group,
|
|
4200
4262
|
body.studio-ui-refresh .studio-refresh-utility-left {
|
|
@@ -4202,7 +4264,7 @@
|
|
|
4202
4264
|
}
|
|
4203
4265
|
|
|
4204
4266
|
body.studio-ui-refresh .studio-refresh-toolbar-state .studio-refresh-action-line {
|
|
4205
|
-
justify-content: flex-
|
|
4267
|
+
justify-content: flex-end;
|
|
4206
4268
|
}
|
|
4207
4269
|
}
|
|
4208
4270
|
|
|
@@ -4274,7 +4336,8 @@
|
|
|
4274
4336
|
}
|
|
4275
4337
|
|
|
4276
4338
|
body.studio-ui-refresh .studio-refresh-header-utility {
|
|
4277
|
-
grid-template-columns: minmax(0, 1fr)
|
|
4339
|
+
grid-template-columns: minmax(0, 1fr);
|
|
4340
|
+
justify-items: start;
|
|
4278
4341
|
}
|
|
4279
4342
|
|
|
4280
4343
|
body.studio-ui-refresh .studio-refresh-pane-identity {
|
|
@@ -4291,12 +4354,18 @@
|
|
|
4291
4354
|
flex-wrap: nowrap;
|
|
4292
4355
|
}
|
|
4293
4356
|
|
|
4294
|
-
body.studio-ui-refresh .studio-refresh-context-group
|
|
4295
|
-
body.studio-ui-refresh .studio-refresh-utility-left {
|
|
4357
|
+
body.studio-ui-refresh .studio-refresh-context-group {
|
|
4296
4358
|
overflow: hidden;
|
|
4297
4359
|
white-space: nowrap;
|
|
4298
4360
|
}
|
|
4299
4361
|
|
|
4362
|
+
body.studio-ui-refresh .studio-refresh-utility-left {
|
|
4363
|
+
overflow: visible;
|
|
4364
|
+
white-space: nowrap;
|
|
4365
|
+
justify-content: flex-start;
|
|
4366
|
+
justify-self: start;
|
|
4367
|
+
}
|
|
4368
|
+
|
|
4300
4369
|
body.studio-ui-refresh .studio-refresh-title-group {
|
|
4301
4370
|
position: relative;
|
|
4302
4371
|
z-index: 2;
|
|
@@ -4400,12 +4469,14 @@
|
|
|
4400
4469
|
border-color: transparent;
|
|
4401
4470
|
background: transparent;
|
|
4402
4471
|
padding: 3px 5px;
|
|
4403
|
-
font-size:
|
|
4472
|
+
font-size: 12px;
|
|
4473
|
+
font-weight: 400;
|
|
4474
|
+
line-height: 1.25;
|
|
4404
4475
|
border-radius: 8px;
|
|
4405
4476
|
}
|
|
4406
4477
|
|
|
4407
4478
|
body.studio-ui-refresh #sourceBadge {
|
|
4408
|
-
color: var(--text);
|
|
4479
|
+
color: var(--studio-info-text, var(--muted));
|
|
4409
4480
|
max-width: min(34rem, 54vw);
|
|
4410
4481
|
min-width: 0;
|
|
4411
4482
|
overflow: hidden;
|
|
@@ -4413,7 +4484,6 @@
|
|
|
4413
4484
|
white-space: nowrap;
|
|
4414
4485
|
}
|
|
4415
4486
|
|
|
4416
|
-
body.studio-ui-refresh #resourceDirBtn,
|
|
4417
4487
|
body.studio-ui-refresh #reviewNotesBtn,
|
|
4418
4488
|
body.studio-ui-refresh #outlineBtn,
|
|
4419
4489
|
body.studio-ui-refresh #scratchpadBtn,
|
|
@@ -4421,11 +4491,19 @@
|
|
|
4421
4491
|
color: var(--text);
|
|
4422
4492
|
}
|
|
4423
4493
|
|
|
4494
|
+
body.studio-ui-refresh #resourceDirBtn,
|
|
4424
4495
|
body.studio-ui-refresh #resourceDirLabel {
|
|
4425
4496
|
color: var(--studio-info-text, var(--muted));
|
|
4426
4497
|
font-weight: 400;
|
|
4427
4498
|
}
|
|
4428
4499
|
|
|
4500
|
+
body.studio-ui-refresh #resourceDirBtn:hover,
|
|
4501
|
+
body.studio-ui-refresh #resourceDirLabel:hover {
|
|
4502
|
+
color: var(--text);
|
|
4503
|
+
background: var(--studio-header-action-hover-bg, var(--panel-2));
|
|
4504
|
+
border-color: transparent;
|
|
4505
|
+
}
|
|
4506
|
+
|
|
4429
4507
|
body.studio-ui-refresh #resourceDirInputWrap.visible {
|
|
4430
4508
|
display: inline-flex;
|
|
4431
4509
|
}
|
|
@@ -4439,9 +4517,11 @@
|
|
|
4439
4517
|
border-radius: 999px;
|
|
4440
4518
|
padding: 2px 7px;
|
|
4441
4519
|
background: transparent;
|
|
4442
|
-
color: var(--muted);
|
|
4443
|
-
opacity:
|
|
4444
|
-
font-
|
|
4520
|
+
color: var(--studio-info-text, var(--muted));
|
|
4521
|
+
opacity: 1;
|
|
4522
|
+
font-size: 12px;
|
|
4523
|
+
font-weight: 400;
|
|
4524
|
+
line-height: 1.25;
|
|
4445
4525
|
}
|
|
4446
4526
|
|
|
4447
4527
|
body.studio-ui-refresh #syncBadge[hidden] {
|
|
@@ -4457,6 +4537,11 @@
|
|
|
4457
4537
|
opacity: 0.72;
|
|
4458
4538
|
}
|
|
4459
4539
|
|
|
4540
|
+
body.studio-ui-refresh #syncBadge.out-of-sync::before {
|
|
4541
|
+
background: var(--warning, var(--accent));
|
|
4542
|
+
opacity: 0.95;
|
|
4543
|
+
}
|
|
4544
|
+
|
|
4460
4545
|
body.studio-ui-refresh #reviewNotesBtn,
|
|
4461
4546
|
body.studio-ui-refresh #outlineBtn,
|
|
4462
4547
|
body.studio-ui-refresh #scratchpadBtn,
|
|
@@ -4474,9 +4559,10 @@
|
|
|
4474
4559
|
body.studio-zen-mode #exportPdfBtn,
|
|
4475
4560
|
body.studio-zen-mode .studio-refresh-tool-tab,
|
|
4476
4561
|
body.studio-zen-mode .studio-refresh-toolbar-state,
|
|
4477
|
-
body.studio-zen-mode .studio-refresh-toolbar-actions .studio-refresh-action-line
|
|
4562
|
+
body.studio-zen-mode .studio-refresh-toolbar-actions .studio-refresh-utility-action-line,
|
|
4478
4563
|
body.studio-zen-mode .source-actions-row:nth-child(n+3),
|
|
4479
4564
|
body.studio-zen-mode #copyDraftBtn,
|
|
4565
|
+
body.studio-zen-mode #suggestCompletionBtn,
|
|
4480
4566
|
body.studio-zen-mode #openCompanionBtn,
|
|
4481
4567
|
body.studio-zen-mode #sendEditorBtn {
|
|
4482
4568
|
display: none !important;
|
|
@@ -4664,6 +4750,12 @@
|
|
|
4664
4750
|
font-size: 12px;
|
|
4665
4751
|
}
|
|
4666
4752
|
|
|
4753
|
+
body.studio-ui-refresh .studio-refresh-toolbar-actions .studio-refresh-action-line button:not(#sendRunBtn):not(#queueSteerBtn):not(#sendReplBtn):not(.request-stop-active) {
|
|
4754
|
+
min-height: 26px;
|
|
4755
|
+
padding: 5px 8px;
|
|
4756
|
+
line-height: 1.2;
|
|
4757
|
+
}
|
|
4758
|
+
|
|
4667
4759
|
body.studio-ui-refresh .studio-refresh-toolbar button:not(#sendRunBtn):not(#queueSteerBtn):not(#sendReplBtn):not(.request-stop-active),
|
|
4668
4760
|
body.studio-ui-refresh .studio-refresh-toolbar select {
|
|
4669
4761
|
color: color-mix(in srgb, var(--text) 72%, var(--muted));
|
|
@@ -4737,6 +4829,19 @@
|
|
|
4737
4829
|
width: min(320px, calc(100vw - 48px));
|
|
4738
4830
|
}
|
|
4739
4831
|
|
|
4832
|
+
body.studio-ui-refresh .studio-refresh-title-group .studio-refresh-context-anchor .studio-refresh-chip {
|
|
4833
|
+
color: var(--studio-info-text, var(--muted));
|
|
4834
|
+
font-size: 13px;
|
|
4835
|
+
font-weight: 500;
|
|
4836
|
+
padding: 3px 5px;
|
|
4837
|
+
}
|
|
4838
|
+
|
|
4839
|
+
body.studio-ui-refresh .studio-refresh-context-anchor .studio-refresh-menu {
|
|
4840
|
+
left: 0;
|
|
4841
|
+
right: auto;
|
|
4842
|
+
width: min(360px, calc(100vw - 48px));
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4740
4845
|
body.studio-ui-refresh .studio-refresh-menu[hidden] {
|
|
4741
4846
|
display: none !important;
|
|
4742
4847
|
}
|
|
@@ -4784,6 +4889,11 @@
|
|
|
4784
4889
|
text-align: left;
|
|
4785
4890
|
}
|
|
4786
4891
|
|
|
4892
|
+
body.studio-ui-refresh .completion-context-option.is-selected {
|
|
4893
|
+
color: var(--text);
|
|
4894
|
+
font-weight: 650;
|
|
4895
|
+
}
|
|
4896
|
+
|
|
4787
4897
|
body.studio-ui-refresh .studio-refresh-menu-item > button:not(:disabled):hover,
|
|
4788
4898
|
body.studio-ui-refresh .studio-refresh-menu-item > select:not(:disabled):hover {
|
|
4789
4899
|
background: var(--studio-header-action-hover-bg, var(--panel-2));
|
package/index.ts
CHANGED
|
@@ -311,6 +311,24 @@ interface SendRunRequestMessage {
|
|
|
311
311
|
text: string;
|
|
312
312
|
}
|
|
313
313
|
|
|
314
|
+
interface CompletionSuggestionRequestMessage {
|
|
315
|
+
type: "completion_suggestion_request";
|
|
316
|
+
requestId: string;
|
|
317
|
+
text: string;
|
|
318
|
+
selectionStart: number;
|
|
319
|
+
selectionEnd: number;
|
|
320
|
+
language?: string;
|
|
321
|
+
label?: string;
|
|
322
|
+
path?: string;
|
|
323
|
+
contextMode?: "cursor" | "session";
|
|
324
|
+
contextText?: string;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
interface CompletionSuggestionCancelRequestMessage {
|
|
328
|
+
type: "completion_suggestion_cancel_request";
|
|
329
|
+
requestId: string;
|
|
330
|
+
}
|
|
331
|
+
|
|
314
332
|
interface QuizGenerateRequestMessage {
|
|
315
333
|
type: "quiz_generate_request";
|
|
316
334
|
requestId: string;
|
|
@@ -451,6 +469,8 @@ type IncomingStudioMessage =
|
|
|
451
469
|
| CritiqueRequestMessage
|
|
452
470
|
| AnnotationRequestMessage
|
|
453
471
|
| SendRunRequestMessage
|
|
472
|
+
| CompletionSuggestionRequestMessage
|
|
473
|
+
| CompletionSuggestionCancelRequestMessage
|
|
454
474
|
| QuizGenerateRequestMessage
|
|
455
475
|
| QuizAnswerRequestMessage
|
|
456
476
|
| QuizDiscussRequestMessage
|
|
@@ -472,6 +492,10 @@ type IncomingStudioMessage =
|
|
|
472
492
|
|
|
473
493
|
const REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
|
|
474
494
|
const PREVIEW_RENDER_MAX_CHARS = 400_000;
|
|
495
|
+
const STUDIO_COMPLETION_MAX_TEXT_CHARS = 250_000;
|
|
496
|
+
const STUDIO_COMPLETION_MAX_CONTEXT_CHARS = 12_000;
|
|
497
|
+
const STUDIO_COMPLETION_PREFIX_CHARS = 12_000;
|
|
498
|
+
const STUDIO_COMPLETION_SUFFIX_CHARS = 6_000;
|
|
475
499
|
const PDF_EXPORT_MAX_CHARS = 400_000;
|
|
476
500
|
const HTML_EXPORT_MAX_CHARS = 400_000;
|
|
477
501
|
const HTML_PREVIEW_MATH_RENDER_MAX_ITEMS = 250;
|
|
@@ -7118,7 +7142,7 @@ async function resolveStudioModelRequestAuth(ctx: StudioModelRequestContext, mod
|
|
|
7118
7142
|
if (typeof registry.getApiKey === "function") {
|
|
7119
7143
|
return { apiKey: await registry.getApiKey(model) };
|
|
7120
7144
|
}
|
|
7121
|
-
throw new Error("Current pi model registry does not expose model credentials for Studio
|
|
7145
|
+
throw new Error("Current pi model registry does not expose model credentials for Studio model requests.");
|
|
7122
7146
|
}
|
|
7123
7147
|
|
|
7124
7148
|
function getStudioQuizReasoning(model: NonNullable<ExtensionContext["model"]>, thinking: StudioQuizThinking | undefined): ThinkingLevel | undefined {
|
|
@@ -7127,33 +7151,47 @@ function getStudioQuizReasoning(model: NonNullable<ExtensionContext["model"]>, t
|
|
|
7127
7151
|
return normalized === "off" ? undefined : normalized;
|
|
7128
7152
|
}
|
|
7129
7153
|
|
|
7130
|
-
async function
|
|
7154
|
+
async function runStudioModelText(
|
|
7155
|
+
ctx: StudioModelRequestContext,
|
|
7156
|
+
prompt: string,
|
|
7157
|
+
options?: { systemPrompt?: string; maxTokens?: number; signal?: AbortSignal; reasoning?: ThinkingLevel; timeoutMs?: number; trim?: boolean },
|
|
7158
|
+
): Promise<string> {
|
|
7131
7159
|
if (!ctx.model) throw new Error("No active model selected.");
|
|
7132
7160
|
const auth = await resolveStudioModelRequestAuth(ctx, ctx.model);
|
|
7133
7161
|
const response = await completeSimple(
|
|
7134
7162
|
ctx.model,
|
|
7135
7163
|
{
|
|
7136
|
-
systemPrompt: "You are
|
|
7164
|
+
systemPrompt: options?.systemPrompt ?? "You are a concise assistant inside pi Studio. Return exactly the requested format.",
|
|
7137
7165
|
messages: [{ role: "user", content: [{ type: "text", text: prompt }], timestamp: Date.now() }],
|
|
7138
7166
|
},
|
|
7139
7167
|
{
|
|
7140
7168
|
apiKey: auth.apiKey,
|
|
7141
7169
|
headers: auth.headers,
|
|
7142
|
-
reasoning:
|
|
7170
|
+
reasoning: options?.reasoning,
|
|
7143
7171
|
maxTokens: options?.maxTokens ?? 2500,
|
|
7144
7172
|
signal: options?.signal,
|
|
7145
|
-
timeoutMs: 120_000,
|
|
7173
|
+
timeoutMs: options?.timeoutMs ?? 120_000,
|
|
7146
7174
|
},
|
|
7147
7175
|
);
|
|
7148
|
-
const
|
|
7176
|
+
const rawText = response.content
|
|
7149
7177
|
.filter((part): part is { type: "text"; text: string } => part.type === "text")
|
|
7150
7178
|
.map((part) => part.text)
|
|
7151
|
-
.join("\n")
|
|
7152
|
-
|
|
7153
|
-
if (!text) throw new Error("Model returned no text response.");
|
|
7179
|
+
.join("\n");
|
|
7180
|
+
const text = options?.trim === false ? rawText : rawText.trim();
|
|
7181
|
+
if (!text.trim()) throw new Error("Model returned no text response.");
|
|
7154
7182
|
return text;
|
|
7155
7183
|
}
|
|
7156
7184
|
|
|
7185
|
+
async function runStudioQuizModelText(ctx: StudioModelRequestContext, prompt: string, options?: { maxTokens?: number; signal?: AbortSignal; thinking?: StudioQuizThinking }): Promise<string> {
|
|
7186
|
+
return runStudioModelText(ctx, prompt, {
|
|
7187
|
+
systemPrompt: "You are an active tutor inside pi Studio. Ask and mark concise, probing quiz questions. Return exactly the requested format.",
|
|
7188
|
+
reasoning: ctx.model ? getStudioQuizReasoning(ctx.model, options?.thinking) : undefined,
|
|
7189
|
+
maxTokens: options?.maxTokens ?? 2500,
|
|
7190
|
+
signal: options?.signal,
|
|
7191
|
+
timeoutMs: 120_000,
|
|
7192
|
+
});
|
|
7193
|
+
}
|
|
7194
|
+
|
|
7157
7195
|
async function runStudioQuizModelJson(
|
|
7158
7196
|
ctx: StudioModelRequestContext,
|
|
7159
7197
|
prompt: string,
|
|
@@ -7181,6 +7219,84 @@ async function runStudioQuizModelJson(
|
|
|
7181
7219
|
throw lastError ?? new Error("Model did not return valid JSON.");
|
|
7182
7220
|
}
|
|
7183
7221
|
|
|
7222
|
+
function buildStudioCompletionSuggestionPrompt(options: {
|
|
7223
|
+
text: string;
|
|
7224
|
+
selectionStart: number;
|
|
7225
|
+
selectionEnd: number;
|
|
7226
|
+
language?: string;
|
|
7227
|
+
label?: string;
|
|
7228
|
+
path?: string;
|
|
7229
|
+
contextMode?: "cursor" | "session";
|
|
7230
|
+
contextText?: string;
|
|
7231
|
+
}): string {
|
|
7232
|
+
const text = String(options.text || "");
|
|
7233
|
+
const start = Math.max(0, Math.min(Math.floor(options.selectionStart || 0), text.length));
|
|
7234
|
+
const end = Math.max(start, Math.min(Math.floor(options.selectionEnd || start), text.length));
|
|
7235
|
+
const prefix = text.slice(Math.max(0, start - STUDIO_COMPLETION_PREFIX_CHARS), start);
|
|
7236
|
+
const selected = text.slice(start, end);
|
|
7237
|
+
const suffix = text.slice(end, Math.min(text.length, end + STUDIO_COMPLETION_SUFFIX_CHARS));
|
|
7238
|
+
const language = String(options.language || "").trim() || "unknown";
|
|
7239
|
+
const label = String(options.label || options.path || "Studio editor").trim();
|
|
7240
|
+
const contextText = String(options.contextText || "").trim().slice(-STUDIO_COMPLETION_MAX_CONTEXT_CHARS);
|
|
7241
|
+
return [
|
|
7242
|
+
"Generate an inline completion for the current editor cursor position.",
|
|
7243
|
+
"Return only the exact text to insert. Do not wrap it in Markdown fences. Do not explain.",
|
|
7244
|
+
"Match the surrounding language, style, indentation, and register.",
|
|
7245
|
+
"Keep the suggestion short unless the context clearly asks for a longer continuation.",
|
|
7246
|
+
contextText
|
|
7247
|
+
? "Use the extra session context only as background. Do not continue the extra context directly unless the editor cursor calls for it."
|
|
7248
|
+
: "Use only the cursor-local editor context below.",
|
|
7249
|
+
selected
|
|
7250
|
+
? "The selected text will be replaced by the completion."
|
|
7251
|
+
: "The completion will be inserted at the cursor.",
|
|
7252
|
+
"",
|
|
7253
|
+
`File/context label: ${label}`,
|
|
7254
|
+
`Language mode: ${language}`,
|
|
7255
|
+
`Suggestion context mode: ${contextText ? "editor plus latest response" : "editor only"}`,
|
|
7256
|
+
contextText ? ["", "<extra_context>", contextText, "</extra_context>"].join("\n") : "",
|
|
7257
|
+
"",
|
|
7258
|
+
"<prefix>",
|
|
7259
|
+
prefix,
|
|
7260
|
+
"</prefix>",
|
|
7261
|
+
selected ? ["", "<selected>", selected, "</selected>"].join("\n") : "",
|
|
7262
|
+
"",
|
|
7263
|
+
"<suffix>",
|
|
7264
|
+
suffix,
|
|
7265
|
+
"</suffix>",
|
|
7266
|
+
].filter((part) => part !== "").join("\n");
|
|
7267
|
+
}
|
|
7268
|
+
|
|
7269
|
+
function cleanStudioCompletionSuggestion(text: string): string {
|
|
7270
|
+
let value = String(text || "").replace(/\r\n/g, "\n");
|
|
7271
|
+
value = value.replace(/^\s*(?:Here(?:'s| is) (?:the )?(?:completion|suggestion):|Completion:|Suggestion:)\s*/i, "");
|
|
7272
|
+
return value;
|
|
7273
|
+
}
|
|
7274
|
+
|
|
7275
|
+
async function runStudioCompletionSuggestion(ctx: StudioModelRequestContext, options: {
|
|
7276
|
+
text: string;
|
|
7277
|
+
selectionStart: number;
|
|
7278
|
+
selectionEnd: number;
|
|
7279
|
+
language?: string;
|
|
7280
|
+
label?: string;
|
|
7281
|
+
path?: string;
|
|
7282
|
+
contextMode?: "cursor" | "session";
|
|
7283
|
+
contextText?: string;
|
|
7284
|
+
signal?: AbortSignal;
|
|
7285
|
+
}): Promise<string> {
|
|
7286
|
+
const prompt = buildStudioCompletionSuggestionPrompt(options);
|
|
7287
|
+
// Intentionally omit `reasoning`: pi-ai treats absent reasoning as off/disabled
|
|
7288
|
+
// where supported. Passing "minimal" would still enable a reasoning path and slow completions.
|
|
7289
|
+
const suggestion = cleanStudioCompletionSuggestion(await runStudioModelText(ctx, prompt, {
|
|
7290
|
+
systemPrompt: "You are an inline autocomplete engine inside pi Studio. Return only text to insert at the cursor. Never explain. Never include Markdown fences unless literal fences are the intended insertion.",
|
|
7291
|
+
maxTokens: 650,
|
|
7292
|
+
timeoutMs: 60_000,
|
|
7293
|
+
trim: false,
|
|
7294
|
+
signal: options.signal,
|
|
7295
|
+
}));
|
|
7296
|
+
if (!suggestion.trim()) throw new Error("Model returned an empty completion suggestion.");
|
|
7297
|
+
return suggestion;
|
|
7298
|
+
}
|
|
7299
|
+
|
|
7184
7300
|
function inferStudioResponseKind(markdown: string): StudioRequestKind {
|
|
7185
7301
|
const lower = markdown.toLowerCase();
|
|
7186
7302
|
if (lower.includes("## critiques") && lower.includes("## document")) return "critique";
|
|
@@ -7590,6 +7706,34 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
7590
7706
|
};
|
|
7591
7707
|
}
|
|
7592
7708
|
|
|
7709
|
+
if (msg.type === "completion_suggestion_cancel_request" && typeof msg.requestId === "string") {
|
|
7710
|
+
return {
|
|
7711
|
+
type: "completion_suggestion_cancel_request",
|
|
7712
|
+
requestId: msg.requestId,
|
|
7713
|
+
};
|
|
7714
|
+
}
|
|
7715
|
+
|
|
7716
|
+
if (msg.type === "completion_suggestion_request" && typeof msg.requestId === "string" && typeof msg.text === "string") {
|
|
7717
|
+
const textLength = msg.text.length;
|
|
7718
|
+
const rawStart = typeof msg.selectionStart === "number" && Number.isFinite(msg.selectionStart) ? msg.selectionStart : textLength;
|
|
7719
|
+
const rawEnd = typeof msg.selectionEnd === "number" && Number.isFinite(msg.selectionEnd) ? msg.selectionEnd : rawStart;
|
|
7720
|
+
const selectionStart = Math.max(0, Math.min(Math.floor(rawStart), textLength));
|
|
7721
|
+
const selectionEnd = Math.max(selectionStart, Math.min(Math.floor(rawEnd), textLength));
|
|
7722
|
+
const contextMode = msg.contextMode === "session" ? "session" : "cursor";
|
|
7723
|
+
return {
|
|
7724
|
+
type: "completion_suggestion_request",
|
|
7725
|
+
requestId: msg.requestId,
|
|
7726
|
+
text: msg.text,
|
|
7727
|
+
selectionStart,
|
|
7728
|
+
selectionEnd,
|
|
7729
|
+
language: typeof msg.language === "string" ? msg.language : undefined,
|
|
7730
|
+
label: typeof msg.label === "string" ? msg.label : undefined,
|
|
7731
|
+
path: typeof msg.path === "string" ? msg.path : undefined,
|
|
7732
|
+
contextMode,
|
|
7733
|
+
contextText: contextMode === "session" && typeof msg.contextText === "string" ? msg.contextText.slice(-STUDIO_COMPLETION_MAX_CONTEXT_CHARS) : undefined,
|
|
7734
|
+
};
|
|
7735
|
+
}
|
|
7736
|
+
|
|
7593
7737
|
if (msg.type === "quiz_generate_request" && typeof msg.requestId === "string" && typeof msg.sourceText === "string") {
|
|
7594
7738
|
const rawCount = typeof msg.questionCount === "number" && Number.isFinite(msg.questionCount) ? msg.questionCount : 5;
|
|
7595
7739
|
return {
|
|
@@ -9467,8 +9611,14 @@ ${cssVarsBlock}
|
|
|
9467
9611
|
</select>
|
|
9468
9612
|
</div>
|
|
9469
9613
|
<div class="source-actions-row">
|
|
9470
|
-
<button id="copyDraftBtn" type="button" title="Copy the current editor text to the clipboard.">Copy
|
|
9471
|
-
<button id="
|
|
9614
|
+
<button id="copyDraftBtn" type="button" title="Copy the current editor text to the clipboard.">Copy</button>
|
|
9615
|
+
<button id="suggestCompletionBtn" type="button" title="Ask the current model for a short completion at the editor cursor. Shortcut: Option/Alt+Tab where available, or Cmd/Ctrl+Shift+Space from the editor.">Suggest</button>
|
|
9616
|
+
<button id="suggestCompletionOptionsBtn" type="button" hidden title="Suggestion context options">▾</button>
|
|
9617
|
+
<select id="completionContextSelect" hidden aria-label="Suggestion context mode" title="Choose how much context Suggest includes.">
|
|
9618
|
+
<option value="cursor" selected>Context: editor only</option>
|
|
9619
|
+
<option value="session">Context: editor + latest response</option>
|
|
9620
|
+
</select>
|
|
9621
|
+
<button id="openCompanionBtn" type="button" title="Open a detached copy of the current editor text in a new editor-only Studio tab.">New editor</button>
|
|
9472
9622
|
<button id="sendEditorBtn" type="button">Send to pi editor</button>
|
|
9473
9623
|
</div>
|
|
9474
9624
|
<div class="source-actions-row">
|
|
@@ -9550,6 +9700,16 @@ ${cssVarsBlock}
|
|
|
9550
9700
|
<button id="editorSelectionJumpBtn" type="button" class="editor-selection-action-btn" hidden title="Jump to the current editor selection in the preview.">Jump</button>
|
|
9551
9701
|
</div>
|
|
9552
9702
|
</div>
|
|
9703
|
+
<div id="completionSuggestionPanel" class="completion-suggestion-panel" hidden>
|
|
9704
|
+
<div class="completion-suggestion-header">
|
|
9705
|
+
<strong>Suggested completion</strong>
|
|
9706
|
+
<button id="completionSuggestionDismissBtn" type="button" title="Dismiss this suggestion">Dismiss</button>
|
|
9707
|
+
</div>
|
|
9708
|
+
<pre id="completionSuggestionText" class="completion-suggestion-text"></pre>
|
|
9709
|
+
<div class="completion-suggestion-actions">
|
|
9710
|
+
<button id="completionSuggestionInsertBtn" type="button" title="Insert this suggestion at the cursor or original selection. You can also press Tab while the editor is focused.">Insert suggestion (Tab)</button>
|
|
9711
|
+
</div>
|
|
9712
|
+
</div>
|
|
9553
9713
|
<div id="sourcePreview" class="panel-scroll rendered-markdown" hidden><pre class="plain-markdown"></pre></div>
|
|
9554
9714
|
</div>
|
|
9555
9715
|
<aside id="outlineOverlay" class="outline-dock-wrap" hidden>
|
|
@@ -9718,7 +9878,10 @@ ${cssVarsBlock}
|
|
|
9718
9878
|
<dl>
|
|
9719
9879
|
<div><dt>Cmd/Ctrl+S</dt><dd>Save editor</dd></div>
|
|
9720
9880
|
<div><dt>Cmd/Ctrl+Enter</dt><dd>Run editor text, or queue steering during an active run</dd></div>
|
|
9721
|
-
<div><dt>Tab /
|
|
9881
|
+
<div><dt>Option/Alt+Tab or Cmd/Ctrl+Shift+Space</dt><dd>Suggest a completion at the editor cursor</dd></div>
|
|
9882
|
+
<div><dt>Tab</dt><dd>Insert a visible completion suggestion; otherwise indent selected editor text</dd></div>
|
|
9883
|
+
<div><dt>Esc</dt><dd>Dismiss a visible completion suggestion, close overlays, exit pane focus, or stop an active request</dd></div>
|
|
9884
|
+
<div><dt>Shift+Tab</dt><dd>Unindent selected editor text</dd></div>
|
|
9722
9885
|
</dl>
|
|
9723
9886
|
</section>
|
|
9724
9887
|
<section class="shortcuts-group">
|
|
@@ -9812,6 +9975,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
9812
9975
|
let studioReplActiveSessionName: string | null = null;
|
|
9813
9976
|
let compactInProgress = false;
|
|
9814
9977
|
let compactRequestId: string | null = null;
|
|
9978
|
+
const activeCompletionSuggestions = new Map<string, AbortController>();
|
|
9815
9979
|
|
|
9816
9980
|
const selectStudioReplSessionForTool = (params: { sessionName?: string; target?: string }): { session: StudioReplSessionInfo | null; error?: string; sessions: StudioReplSessionInfo[] } => {
|
|
9817
9981
|
const state = listStudioReplSessions();
|
|
@@ -11298,6 +11462,77 @@ export default function (pi: ExtensionAPI) {
|
|
|
11298
11462
|
return;
|
|
11299
11463
|
}
|
|
11300
11464
|
|
|
11465
|
+
if (msg.type === "completion_suggestion_cancel_request") {
|
|
11466
|
+
if (!isValidRequestId(msg.requestId)) {
|
|
11467
|
+
sendToClient(client, { type: "completion_suggestion_error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
11468
|
+
return;
|
|
11469
|
+
}
|
|
11470
|
+
const controller = activeCompletionSuggestions.get(msg.requestId);
|
|
11471
|
+
if (!controller) {
|
|
11472
|
+
sendToClient(client, { type: "completion_suggestion_error", requestId: msg.requestId, message: "No matching suggestion request is running." });
|
|
11473
|
+
return;
|
|
11474
|
+
}
|
|
11475
|
+
controller.abort();
|
|
11476
|
+
sendToClient(client, { type: "completion_suggestion_progress", requestId: msg.requestId, message: "Stopping suggestion…" });
|
|
11477
|
+
return;
|
|
11478
|
+
}
|
|
11479
|
+
|
|
11480
|
+
if (msg.type === "completion_suggestion_request") {
|
|
11481
|
+
if (!isValidRequestId(msg.requestId)) {
|
|
11482
|
+
sendToClient(client, { type: "completion_suggestion_error", requestId: msg.requestId, message: "Invalid request ID." });
|
|
11483
|
+
return;
|
|
11484
|
+
}
|
|
11485
|
+
const ctx = latestModelRequestCtx ?? lastCommandCtx;
|
|
11486
|
+
if (!ctx) {
|
|
11487
|
+
sendToClient(client, { type: "completion_suggestion_error", requestId: msg.requestId, message: "No active pi model context is available for editor suggestions." });
|
|
11488
|
+
return;
|
|
11489
|
+
}
|
|
11490
|
+
if (!msg.text.trim()) {
|
|
11491
|
+
sendToClient(client, { type: "completion_suggestion_error", requestId: msg.requestId, message: "Editor is empty." });
|
|
11492
|
+
return;
|
|
11493
|
+
}
|
|
11494
|
+
if (msg.text.length > STUDIO_COMPLETION_MAX_TEXT_CHARS) {
|
|
11495
|
+
sendToClient(client, { type: "completion_suggestion_error", requestId: msg.requestId, message: `Editor text is too large for suggestions (${STUDIO_COMPLETION_MAX_TEXT_CHARS} character limit).` });
|
|
11496
|
+
return;
|
|
11497
|
+
}
|
|
11498
|
+
sendToClient(client, { type: "completion_suggestion_progress", requestId: msg.requestId, message: "Generating suggestion…" });
|
|
11499
|
+
const completionController = new AbortController();
|
|
11500
|
+
activeCompletionSuggestions.set(msg.requestId, completionController);
|
|
11501
|
+
void (async () => {
|
|
11502
|
+
try {
|
|
11503
|
+
const suggestion = await runStudioCompletionSuggestion(ctx, {
|
|
11504
|
+
text: msg.text,
|
|
11505
|
+
selectionStart: msg.selectionStart,
|
|
11506
|
+
selectionEnd: msg.selectionEnd,
|
|
11507
|
+
language: msg.language,
|
|
11508
|
+
label: msg.label,
|
|
11509
|
+
path: msg.path,
|
|
11510
|
+
contextMode: msg.contextMode,
|
|
11511
|
+
contextText: msg.contextText,
|
|
11512
|
+
signal: completionController.signal,
|
|
11513
|
+
});
|
|
11514
|
+
sendToClient(client, {
|
|
11515
|
+
type: "completion_suggestion_result",
|
|
11516
|
+
requestId: msg.requestId,
|
|
11517
|
+
suggestion,
|
|
11518
|
+
selectionStart: msg.selectionStart,
|
|
11519
|
+
selectionEnd: msg.selectionEnd,
|
|
11520
|
+
});
|
|
11521
|
+
} catch (error) {
|
|
11522
|
+
sendToClient(client, {
|
|
11523
|
+
type: "completion_suggestion_error",
|
|
11524
|
+
requestId: msg.requestId,
|
|
11525
|
+
message: completionController.signal.aborted
|
|
11526
|
+
? "Suggestion stopped."
|
|
11527
|
+
: `Suggestion failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
11528
|
+
});
|
|
11529
|
+
} finally {
|
|
11530
|
+
activeCompletionSuggestions.delete(msg.requestId);
|
|
11531
|
+
}
|
|
11532
|
+
})();
|
|
11533
|
+
return;
|
|
11534
|
+
}
|
|
11535
|
+
|
|
11301
11536
|
if (msg.type === "quiz_generate_request") {
|
|
11302
11537
|
if (!isValidRequestId(msg.requestId)) {
|
|
11303
11538
|
sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.17",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, active quiz, prompt/response history, live previews, and tmux-backed REPL/literate REPL workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|