pi-studio 0.9.14 → 0.9.16

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.
@@ -114,6 +114,11 @@
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 completionSuggestionPanelEl = document.getElementById("completionSuggestionPanel");
119
+ const completionSuggestionTextEl = document.getElementById("completionSuggestionText");
120
+ const completionSuggestionInsertBtn = document.getElementById("completionSuggestionInsertBtn");
121
+ const completionSuggestionDismissBtn = document.getElementById("completionSuggestionDismissBtn");
117
122
  const saveAnnotatedBtn = document.getElementById("saveAnnotatedBtn");
118
123
  const stripAnnotationsBtn = document.getElementById("stripAnnotationsBtn");
119
124
  const highlightSelect = document.getElementById("highlightSelect");
@@ -1943,6 +1948,11 @@
1943
1948
  let editorHighlightEnabled = false;
1944
1949
  let editorLanguage = "markdown";
1945
1950
  let responseHighlightEnabled = false;
1951
+ let completionSuggestionState = null;
1952
+ let completionSuggestionInFlight = false;
1953
+ let completionSuggestionRequestId = null;
1954
+ let completionSuggestionPendingSnapshot = null;
1955
+ let completionSuggestionRefocusEditorOnResult = false;
1946
1956
  let editorHighlightRenderRaf = null;
1947
1957
  let lineNumbersEnabled = false;
1948
1958
  let lineNumbersRenderRaf = null;
@@ -1955,6 +1965,19 @@
1955
1965
  const DEFAULT_RESPONSE_FONT_SIZE = studioUiRefreshEnabled ? 13.5 : 15;
1956
1966
  let editorFontSize = DEFAULT_EDITOR_FONT_SIZE;
1957
1967
  let responseFontSize = DEFAULT_RESPONSE_FONT_SIZE;
1968
+ let fileBrowserState = {
1969
+ rootDir: "",
1970
+ currentDir: "",
1971
+ relativeDir: "",
1972
+ parentDir: null,
1973
+ entries: [],
1974
+ omitted: 0,
1975
+ omittedIgnored: 0,
1976
+ loading: false,
1977
+ error: "",
1978
+ loaded: false,
1979
+ };
1980
+ let fileBrowserLoadNonce = 0;
1958
1981
  let studioUiRefreshUi = null;
1959
1982
  let studioZenModeEnabled = readStudioZenModeEnabled();
1960
1983
  if (studioUiRefreshEnabled && document.body) {
@@ -2385,6 +2408,7 @@
2385
2408
  if (!isEditorOnlyMode && replSendModeSelect) replActionLineEl.appendChild(replSendModeSelect);
2386
2409
  const actionLineTwoEl = makeStudioUiRefreshElement("div", "studio-refresh-action-line");
2387
2410
  actionLineTwoEl.appendChild(copyDraftBtn);
2411
+ if (suggestCompletionBtn) actionLineTwoEl.appendChild(suggestCompletionBtn);
2388
2412
  if (openCompanionBtn) actionLineTwoEl.appendChild(openCompanionBtn);
2389
2413
  if (!isEditorOnlyMode && sendEditorBtn) actionLineTwoEl.appendChild(sendEditorBtn);
2390
2414
  if (actionLineOneEl.childNodes.length > 0) actionsEl.appendChild(actionLineOneEl);
@@ -3667,6 +3691,8 @@
3667
3691
  return;
3668
3692
  }
3669
3693
 
3694
+ if (handleCompletionSuggestionAcceptKey(event)) return;
3695
+
3670
3696
  if ((key === "?" || (key === "/" && event.shiftKey)) && !event.metaKey && !event.ctrlKey && !event.altKey && !isTextEntryShortcutTarget(event.target)) {
3671
3697
  event.preventDefault();
3672
3698
  toggleShortcuts();
@@ -4085,6 +4111,12 @@
4085
4111
  }
4086
4112
  if (referenceMetaEl instanceof HTMLElement) referenceMetaEl.hidden = false;
4087
4113
 
4114
+ if (rightView === "files") {
4115
+ const dir = fileBrowserState && fileBrowserState.currentDir ? fileBrowserState.currentDir : (getCurrentResourceDirValue() || "current Studio directory");
4116
+ referenceBadgeEl.textContent = "Files: " + dir;
4117
+ return;
4118
+ }
4119
+
4088
4120
  if (rightView === "trace") {
4089
4121
  const state = traceState || createEmptyTraceState();
4090
4122
  const context = traceDisplayContext || {};
@@ -6645,6 +6677,7 @@
6645
6677
  critiqueViewEl.addEventListener("scroll", handleTracePaneScroll);
6646
6678
  critiqueViewEl.addEventListener("click", handleTracePaneClick);
6647
6679
  critiqueViewEl.addEventListener("click", handleReplPaneClick);
6680
+ critiqueViewEl.addEventListener("click", handleFilesPaneClick);
6648
6681
  critiqueViewEl.addEventListener("change", handleReplPaneChange);
6649
6682
  }
6650
6683
 
@@ -8107,6 +8140,241 @@
8107
8140
  scheduleResponsePaneRepaintNudge();
8108
8141
  }
8109
8142
 
8143
+ function getFileBrowserContextKey() {
8144
+ const context = getHtmlPreviewResourceContextOptions();
8145
+ return String(context.sourcePath || "") + "\n" + String(context.resourceDir || "");
8146
+ }
8147
+
8148
+ function getFileBrowserLocalLinkContext() {
8149
+ return { sourcePath: "", resourceDir: fileBrowserState.rootDir || getCurrentResourceDirValue() || "" };
8150
+ }
8151
+
8152
+ function formatFileBrowserSize(size) {
8153
+ const value = Number(size);
8154
+ if (!Number.isFinite(value) || value < 0) return "";
8155
+ if (value < 1024) return Math.round(value) + " B";
8156
+ if (value < 1024 * 1024) return (value / 1024).toFixed(value < 10 * 1024 ? 1 : 0) + " KB";
8157
+ if (value < 1024 * 1024 * 1024) return (value / (1024 * 1024)).toFixed(value < 10 * 1024 * 1024 ? 1 : 0) + " MB";
8158
+ return (value / (1024 * 1024 * 1024)).toFixed(1) + " GB";
8159
+ }
8160
+
8161
+ function formatFileBrowserTime(ms) {
8162
+ const value = Number(ms);
8163
+ if (!Number.isFinite(value) || value <= 0) return "";
8164
+ try {
8165
+ return new Date(value).toLocaleDateString([], { month: "short", day: "numeric", year: "numeric" });
8166
+ } catch {
8167
+ return "";
8168
+ }
8169
+ }
8170
+
8171
+ function getFileBrowserKindLabel(entry) {
8172
+ if (!entry || entry.type === "directory") return "folder";
8173
+ if (entry.kind === "text") return "document";
8174
+ if (entry.kind === "pdf") return "PDF";
8175
+ if (entry.kind === "image") return "image";
8176
+ return entry.extension ? entry.extension.replace(/^\./, "") : "file";
8177
+ }
8178
+
8179
+ function buildFileBrowserPanelHtml() {
8180
+ const state = fileBrowserState || {};
8181
+ const entries = Array.isArray(state.entries) ? state.entries : [];
8182
+ const currentDir = state.currentDir || "";
8183
+ const rootDir = state.rootDir || "";
8184
+ const relativeDir = state.relativeDir || ".";
8185
+ const parentDisabled = state.parentDir ? "" : " disabled";
8186
+ const rows = entries.length
8187
+ ? entries.map((entry) => {
8188
+ const type = entry.type === "directory" ? "directory" : "file";
8189
+ const kind = entry.kind || (type === "directory" ? "directory" : "other");
8190
+ const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" ? "📝" : "📦")));
8191
+ const metaParts = [];
8192
+ metaParts.push(getFileBrowserKindLabel(entry));
8193
+ if (type === "file") metaParts.push(formatFileBrowserSize(entry.size));
8194
+ const time = formatFileBrowserTime(entry.mtimeMs);
8195
+ if (time) metaParts.push(time);
8196
+ const textActions = kind === "text"
8197
+ ? "<button type='button' data-files-action='open-new' data-files-path='" + escapeHtml(entry.path) + "'>New tab</button>"
8198
+ : "";
8199
+ const openTitle = type === "directory"
8200
+ ? "Open folder"
8201
+ : (kind === "text" ? "Open in editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file")));
8202
+ return "<div class='files-row files-row-" + escapeHtml(type) + " files-kind-" + escapeHtml(kind) + "'>"
8203
+ + "<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) + "'>"
8204
+ + "<span class='files-icon' aria-hidden='true'>" + icon + "</span>"
8205
+ + "<span class='files-name'>" + escapeHtml(entry.name) + "</span>"
8206
+ + "<span class='files-meta'>" + escapeHtml(metaParts.filter(Boolean).join(" · ")) + "</span>"
8207
+ + "</button>"
8208
+ + "<span class='files-actions'>"
8209
+ + textActions
8210
+ + "<button type='button' data-files-action='copy-path' data-files-path='" + escapeHtml(entry.path) + "'>Copy path</button>"
8211
+ + (type === "file" ? "<button type='button' data-files-action='reveal' data-files-path='" + escapeHtml(entry.path) + "'>Reveal</button>" : "")
8212
+ + "</span>"
8213
+ + "</div>";
8214
+ }).join("")
8215
+ : "<div class='files-empty'>" + (state.loading ? "Loading files…" : "This folder is empty.") + "</div>";
8216
+ const notices = [];
8217
+ if (state.error) notices.push("<div class='files-notice files-notice-error'>" + escapeHtml(state.error) + "</div>");
8218
+ if (state.omitted) notices.push("<div class='files-notice'>" + escapeHtml(String(state.omitted)) + " item" + (state.omitted === 1 ? "" : "s") + " omitted.</div>");
8219
+ if (state.omittedIgnored) notices.push("<div class='files-notice'>" + escapeHtml(String(state.omittedIgnored)) + " heavy/cache folder" + (state.omittedIgnored === 1 ? "" : "s") + " hidden.</div>");
8220
+ return "<div class='files-panel'>"
8221
+ + "<div class='files-toolbar'>"
8222
+ + "<div class='files-path-group'><span class='files-label'>Files</span><span class='files-path' title='" + escapeHtml(currentDir) + "'>" + escapeHtml(relativeDir || ".") + "</span></div>"
8223
+ + "<div class='files-toolbar-actions'>"
8224
+ + "<button type='button' data-files-action='parent'" + parentDisabled + ">Parent</button>"
8225
+ + "<button type='button' data-files-action='refresh'>Refresh</button>"
8226
+ + (rootDir ? "<button type='button' data-files-action='copy-root' data-files-path='" + escapeHtml(rootDir) + "'>Copy root</button>" : "")
8227
+ + "</div>"
8228
+ + "</div>"
8229
+ + "<div class='files-subtitle'>Root: <span title='" + escapeHtml(rootDir) + "'>" + escapeHtml(rootDir || "current Studio directory") + "</span></div>"
8230
+ + notices.join("")
8231
+ + "<div class='files-list' role='list'>" + rows + "</div>"
8232
+ + "</div>";
8233
+ }
8234
+
8235
+ function renderFilesView() {
8236
+ if (!critiqueViewEl) return;
8237
+ const contextKey = getFileBrowserContextKey();
8238
+ if (fileBrowserState.contextKey !== contextKey) {
8239
+ fileBrowserState = {
8240
+ rootDir: "",
8241
+ currentDir: "",
8242
+ relativeDir: "",
8243
+ parentDir: null,
8244
+ entries: [],
8245
+ omitted: 0,
8246
+ omittedIgnored: 0,
8247
+ loading: false,
8248
+ error: "",
8249
+ loaded: false,
8250
+ contextKey,
8251
+ };
8252
+ }
8253
+ finishPreviewRender(critiqueViewEl);
8254
+ critiqueViewEl.innerHTML = buildFileBrowserPanelHtml();
8255
+ critiqueViewEl.classList.remove("response-scroll-resetting");
8256
+ if (!fileBrowserState.loaded && !fileBrowserState.loading) {
8257
+ loadFileBrowserDirectory("");
8258
+ }
8259
+ scheduleResponsePaneRepaintNudge();
8260
+ }
8261
+
8262
+ async function loadFileBrowserDirectory(dir, options) {
8263
+ const context = getHtmlPreviewResourceContextOptions();
8264
+ const contextKey = getFileBrowserContextKey();
8265
+ const nonce = ++fileBrowserLoadNonce;
8266
+ fileBrowserState = {
8267
+ ...fileBrowserState,
8268
+ contextKey,
8269
+ loading: true,
8270
+ error: "",
8271
+ };
8272
+ if (rightView === "files") {
8273
+ finishPreviewRender(critiqueViewEl);
8274
+ critiqueViewEl.innerHTML = buildFileBrowserPanelHtml();
8275
+ }
8276
+ try {
8277
+ const query = {};
8278
+ if (dir) query.dir = String(dir);
8279
+ if (context.sourcePath) query.sourcePath = context.sourcePath;
8280
+ if (context.resourceDir) query.resourceDir = context.resourceDir;
8281
+ const payload = await fetchStudioJson("/file-browser", { query });
8282
+ if (nonce !== fileBrowserLoadNonce) return;
8283
+ fileBrowserState = {
8284
+ rootDir: typeof payload.rootDir === "string" ? payload.rootDir : "",
8285
+ currentDir: typeof payload.currentDir === "string" ? payload.currentDir : "",
8286
+ relativeDir: typeof payload.relativeDir === "string" ? payload.relativeDir : ".",
8287
+ parentDir: typeof payload.parentDir === "string" ? payload.parentDir : null,
8288
+ entries: Array.isArray(payload.entries) ? payload.entries : [],
8289
+ omitted: Number(payload.omitted) || 0,
8290
+ omittedIgnored: Number(payload.omittedIgnored) || 0,
8291
+ loading: false,
8292
+ error: "",
8293
+ loaded: true,
8294
+ contextKey,
8295
+ };
8296
+ if (rightView === "files") {
8297
+ finishPreviewRender(critiqueViewEl);
8298
+ critiqueViewEl.innerHTML = buildFileBrowserPanelHtml();
8299
+ scheduleResponsePaneRepaintNudge();
8300
+ }
8301
+ if (options && options.user) setStatus("Loaded file list.", "success");
8302
+ } catch (error) {
8303
+ if (nonce !== fileBrowserLoadNonce) return;
8304
+ fileBrowserState = {
8305
+ ...fileBrowserState,
8306
+ loading: false,
8307
+ error: (error && error.message) ? error.message : String(error || "Could not load files."),
8308
+ loaded: true,
8309
+ };
8310
+ if (rightView === "files") {
8311
+ finishPreviewRender(critiqueViewEl);
8312
+ critiqueViewEl.innerHTML = buildFileBrowserPanelHtml();
8313
+ scheduleResponsePaneRepaintNudge();
8314
+ }
8315
+ }
8316
+ }
8317
+
8318
+ async function openFileBrowserEntry(path, kind) {
8319
+ const context = getFileBrowserLocalLinkContext();
8320
+ if (kind === "text") {
8321
+ await openPreviewDocumentHere(path, context);
8322
+ return;
8323
+ }
8324
+ if (kind === "pdf") {
8325
+ openPreviewPdfLink(path, path, context);
8326
+ return;
8327
+ }
8328
+ if (kind === "image") {
8329
+ await openPreviewImageLink(path, path, context);
8330
+ return;
8331
+ }
8332
+ setStatus("No Studio preview for this file type. Use Copy path or Reveal.", "warning");
8333
+ }
8334
+
8335
+ async function handleFilesPaneClick(event) {
8336
+ if (rightView !== "files") return;
8337
+ const target = event.target;
8338
+ const actionEl = target instanceof Element ? target.closest("[data-files-action]") : null;
8339
+ if (!actionEl) return;
8340
+ event.preventDefault();
8341
+ const action = actionEl.getAttribute("data-files-action") || "";
8342
+ const path = actionEl.getAttribute("data-files-path") || "";
8343
+ const kind = actionEl.getAttribute("data-files-kind") || getPreviewLocalLinkKind(path);
8344
+ try {
8345
+ if (action === "parent") {
8346
+ if (fileBrowserState.parentDir) await loadFileBrowserDirectory(fileBrowserState.parentDir, { user: true });
8347
+ return;
8348
+ }
8349
+ if (action === "refresh") {
8350
+ await loadFileBrowserDirectory(fileBrowserState.currentDir || "", { user: true });
8351
+ return;
8352
+ }
8353
+ if (action === "open-dir") {
8354
+ await loadFileBrowserDirectory(path, { user: true });
8355
+ return;
8356
+ }
8357
+ if (action === "open") {
8358
+ await openFileBrowserEntry(path, kind);
8359
+ return;
8360
+ }
8361
+ if (action === "open-new") {
8362
+ await openPreviewDocumentInNewEditor(path, null, getFileBrowserLocalLinkContext());
8363
+ return;
8364
+ }
8365
+ if (action === "copy-path" || action === "copy-root") {
8366
+ const ok = await writeTextToClipboard(path);
8367
+ setStatus(ok ? "Copied path." : "Clipboard write failed.", ok ? "success" : "warning");
8368
+ return;
8369
+ }
8370
+ if (action === "reveal") {
8371
+ await revealPreviewLocalLink(path, getFileBrowserLocalLinkContext());
8372
+ }
8373
+ } catch (error) {
8374
+ setStatus((error && error.message) ? error.message : String(error || "File action failed."), "warning");
8375
+ }
8376
+ }
8377
+
8110
8378
  function renderActiveResult() {
8111
8379
  if (rightView === "trace") {
8112
8380
  renderTraceView();
@@ -8118,6 +8386,11 @@
8118
8386
  return;
8119
8387
  }
8120
8388
 
8389
+ if (rightView === "files") {
8390
+ renderFilesView();
8391
+ return;
8392
+ }
8393
+
8121
8394
  if (rightView === "editor-preview") {
8122
8395
  const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
8123
8396
  if (!editorText.trim()) {
@@ -8192,7 +8465,7 @@
8192
8465
  : normalizeForCompare(sourceTextEl.value);
8193
8466
  const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
8194
8467
  const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
8195
- const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl";
8468
+ const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl" || rightView === "files";
8196
8469
 
8197
8470
  if (responseWrapEl) {
8198
8471
  responseWrapEl.hidden = showingAuxiliaryRightPane;
@@ -8233,6 +8506,8 @@
8233
8506
  : (exportingReplJournal ? "Export record" : "Export right preview");
8234
8507
  if (rightView === "trace") {
8235
8508
  exportPdfBtn.title = "Working view does not support preview export.";
8509
+ } else if (rightView === "files") {
8510
+ exportPdfBtn.title = "Files view does not support preview export.";
8236
8511
  } else if (exportingReplJournal && !replJournalExportEntries.length) {
8237
8512
  exportPdfBtn.title = "No Studio REPL record entries to export for this session yet.";
8238
8513
  } else if (rightView === "markdown") {
@@ -8391,6 +8666,10 @@
8391
8666
  if (loadGitDiffBtn) loadGitDiffBtn.disabled = uiBusy;
8392
8667
  syncRunAndCritiqueButtons();
8393
8668
  copyDraftBtn.disabled = uiBusy;
8669
+ if (suggestCompletionBtn) {
8670
+ suggestCompletionBtn.disabled = uiBusy || completionSuggestionInFlight || wsState !== "Ready" || !String(sourceTextEl.value || "").trim();
8671
+ suggestCompletionBtn.textContent = completionSuggestionInFlight ? "Suggesting…" : "Suggest";
8672
+ }
8394
8673
  if (openCompanionBtn) openCompanionBtn.disabled = uiBusy || wsState !== "Ready";
8395
8674
  if (highlightSelect) highlightSelect.disabled = uiBusy;
8396
8675
  if (lineNumbersSelect) lineNumbersSelect.disabled = uiBusy;
@@ -8465,9 +8744,24 @@
8465
8744
  return "source:" + normalized.source + ":" + normalized.label;
8466
8745
  }
8467
8746
 
8747
+ function getWorkspacePersistenceStorage() {
8748
+ try {
8749
+ return window.sessionStorage || null;
8750
+ } catch {
8751
+ return null;
8752
+ }
8753
+ }
8754
+
8755
+ function clearLegacyWorkspacePersistenceStorage() {
8756
+ try {
8757
+ if (window.localStorage) window.localStorage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
8758
+ } catch {}
8759
+ }
8760
+
8468
8761
  function readPersistedWorkspaceState() {
8469
8762
  try {
8470
- const raw = window.localStorage ? window.localStorage.getItem(STUDIO_WORKSPACE_STORAGE_KEY) : null;
8763
+ const storage = getWorkspacePersistenceStorage();
8764
+ const raw = storage ? storage.getItem(STUDIO_WORKSPACE_STORAGE_KEY) : null;
8471
8765
  if (!raw) return null;
8472
8766
  const parsed = JSON.parse(raw);
8473
8767
  if (!parsed || typeof parsed !== "object" || parsed.version !== 1) return null;
@@ -8495,7 +8789,7 @@
8495
8789
  sourceState: normalizeWorkspaceSourceState(sourceState),
8496
8790
  resourceDir: getCurrentResourceDirValue(),
8497
8791
  editorView,
8498
- rightView,
8792
+ rightView: isEditorOnlyMode ? "editor-preview" : rightView,
8499
8793
  editorLanguage,
8500
8794
  followLatest,
8501
8795
  responseHistoryIndex,
@@ -8509,13 +8803,15 @@
8509
8803
  function persistWorkspaceStateNow() {
8510
8804
  if (!workspacePersistenceReady) return;
8511
8805
  try {
8512
- if (!window.localStorage) return;
8806
+ const storage = getWorkspacePersistenceStorage();
8807
+ if (!storage) return;
8808
+ clearLegacyWorkspacePersistenceStorage();
8513
8809
  const payload = buildWorkspacePersistencePayload();
8514
8810
  if (payload.text.length > STUDIO_WORKSPACE_MAX_TEXT_CHARS) {
8515
- window.localStorage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
8811
+ storage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
8516
8812
  return;
8517
8813
  }
8518
- window.localStorage.setItem(STUDIO_WORKSPACE_STORAGE_KEY, JSON.stringify(payload));
8814
+ storage.setItem(STUDIO_WORKSPACE_STORAGE_KEY, JSON.stringify(payload));
8519
8815
  } catch {
8520
8816
  // Ignore browser storage failures and quota limits.
8521
8817
  }
@@ -8544,8 +8840,10 @@
8544
8840
  workspacePersistTimer = null;
8545
8841
  }
8546
8842
  try {
8547
- if (window.localStorage) window.localStorage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
8843
+ const storage = getWorkspacePersistenceStorage();
8844
+ if (storage) storage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
8548
8845
  } catch {}
8846
+ clearLegacyWorkspacePersistenceStorage();
8549
8847
  }
8550
8848
 
8551
8849
  function applyPersistedWorkspaceState(state) {
@@ -8563,11 +8861,15 @@
8563
8861
  setEditorLanguage(state.editorLanguage.trim());
8564
8862
  }
8565
8863
  editorView = state.editorView === "preview" ? "preview" : "markdown";
8566
- rightView = state.rightView === "preview"
8567
- ? "preview"
8568
- : (state.rightView === "editor-preview"
8569
- ? "editor-preview"
8570
- : (state.rightView === "repl" ? "repl" : ((state.rightView === "trace" || state.rightView === "thinking") ? "trace" : "markdown")));
8864
+ rightView = isEditorOnlyMode
8865
+ ? "editor-preview"
8866
+ : (state.rightView === "preview"
8867
+ ? "preview"
8868
+ : (state.rightView === "editor-preview"
8869
+ ? "editor-preview"
8870
+ : (state.rightView === "repl"
8871
+ ? "repl"
8872
+ : (state.rightView === "files" ? "files" : ((state.rightView === "trace" || state.rightView === "thinking") ? "trace" : "markdown")))));
8571
8873
  if (typeof state.followLatest === "boolean") {
8572
8874
  followLatest = state.followLatest;
8573
8875
  }
@@ -8592,7 +8894,7 @@
8592
8894
  setStatus("Studio is busy.", "warning");
8593
8895
  return;
8594
8896
  }
8595
- const confirmed = window.confirm("Clear the current editor draft in this browser tab? Saved files and responses are not changed.");
8897
+ const confirmed = window.confirm("Reset the editor to a fresh blank draft in this browser tab? Saved files and responses are not changed.");
8596
8898
  if (!confirmed) return;
8597
8899
  const preservedResponseState = {
8598
8900
  responseHistory: Array.isArray(responseHistory) ? responseHistory.slice() : [],
@@ -8634,7 +8936,7 @@
8634
8936
  if (followSelect) followSelect.value = followLatest ? "on" : "off";
8635
8937
  refreshResponseUi();
8636
8938
  persistWorkspaceStateNow();
8637
- setStatus("Editor cleared. Saved files and responses were not changed.", "success");
8939
+ setStatus("Editor reset to a fresh blank draft. Saved files and responses were not changed.", "success");
8638
8940
  }
8639
8941
 
8640
8942
  function setEditorText(nextText, options) {
@@ -8701,6 +9003,197 @@
8701
9003
  }
8702
9004
  }
8703
9005
 
9006
+ function hideCompletionSuggestion() {
9007
+ completionSuggestionState = null;
9008
+ if (completionSuggestionTextEl) completionSuggestionTextEl.textContent = "";
9009
+ if (completionSuggestionPanelEl) completionSuggestionPanelEl.hidden = true;
9010
+ }
9011
+
9012
+ function showCompletionSuggestion(state) {
9013
+ completionSuggestionState = state;
9014
+ if (completionSuggestionTextEl) completionSuggestionTextEl.textContent = state && state.suggestion ? state.suggestion : "";
9015
+ if (completionSuggestionPanelEl) completionSuggestionPanelEl.hidden = false;
9016
+ }
9017
+
9018
+ function focusSourceTextNoScroll() {
9019
+ if (!sourceTextEl || typeof sourceTextEl.focus !== "function") return;
9020
+ try {
9021
+ sourceTextEl.focus({ preventScroll: true });
9022
+ } catch {
9023
+ try { sourceTextEl.focus(); } catch {}
9024
+ }
9025
+ }
9026
+
9027
+ function focusSourceEditorForCompletion() {
9028
+ const snapshot = snapshotStudioScrollablePositions();
9029
+ if (editorView !== "markdown") {
9030
+ setEditorView("markdown");
9031
+ scheduleStudioScrollablePositionRestore(snapshot);
9032
+ }
9033
+ window.setTimeout(focusSourceTextNoScroll, 0);
9034
+ }
9035
+
9036
+ function isCompletionSuggestionRequestShortcut(event) {
9037
+ if (!event) return false;
9038
+ const key = typeof event.key === "string" ? event.key : "";
9039
+ const code = typeof event.code === "string" ? event.code : "";
9040
+ const commandSpace = (event.metaKey || event.ctrlKey)
9041
+ && event.shiftKey
9042
+ && !event.altKey
9043
+ && (code === "Space" || key === " " || key === "Spacebar");
9044
+ const optionTab = event.altKey
9045
+ && !event.metaKey
9046
+ && !event.ctrlKey
9047
+ && !event.shiftKey
9048
+ && key === "Tab";
9049
+ return commandSpace || optionTab;
9050
+ }
9051
+
9052
+ function handleCompletionSuggestionAcceptKey(event) {
9053
+ if (!event || !completionSuggestionState) return false;
9054
+ if (event.key !== "Tab" || event.shiftKey || event.metaKey || event.ctrlKey || event.altKey) return false;
9055
+ const target = event.target;
9056
+ const focusIsUnclaimed = target === document.body || target === document.documentElement;
9057
+ const targetCanAccept = focusIsUnclaimed
9058
+ || target === sourceTextEl
9059
+ || target === suggestCompletionBtn
9060
+ || Boolean(completionSuggestionPanelEl && target instanceof Element && completionSuggestionPanelEl.contains(target));
9061
+ if (!targetCanAccept) return false;
9062
+ event.preventDefault();
9063
+ insertCompletionSuggestion();
9064
+ return true;
9065
+ }
9066
+
9067
+ function shouldRefocusEditorForCompletionRequest() {
9068
+ const activeEl = document.activeElement;
9069
+ return activeEl === sourceTextEl
9070
+ || activeEl === suggestCompletionBtn
9071
+ || activeEl === document.body
9072
+ || activeEl === document.documentElement
9073
+ || Boolean(completionSuggestionPanelEl && activeEl instanceof Element && completionSuggestionPanelEl.contains(activeEl));
9074
+ }
9075
+
9076
+ function requestCompletionSuggestion() {
9077
+ if (isEditorOnlyMode && !sourceTextEl) return;
9078
+ if (completionSuggestionInFlight) {
9079
+ setStatus("Suggestion request already in progress.", "warning");
9080
+ return;
9081
+ }
9082
+ const text = String(sourceTextEl.value || "");
9083
+ if (!text.trim()) {
9084
+ setStatus("Editor is empty.", "warning");
9085
+ return;
9086
+ }
9087
+ const selectionStart = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : text.length;
9088
+ const selectionEnd = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : selectionStart;
9089
+ const requestId = makeRequestId();
9090
+ completionSuggestionInFlight = true;
9091
+ completionSuggestionRequestId = requestId;
9092
+ completionSuggestionPendingSnapshot = { text, selectionStart, selectionEnd };
9093
+ completionSuggestionRefocusEditorOnResult = shouldRefocusEditorForCompletionRequest();
9094
+ hideCompletionSuggestion();
9095
+ syncActionButtons();
9096
+ setStatus("Generating completion suggestion…", "warning");
9097
+ const sent = sendMessage({
9098
+ type: "completion_suggestion_request",
9099
+ requestId,
9100
+ text,
9101
+ selectionStart,
9102
+ selectionEnd,
9103
+ language: editorLanguage || "",
9104
+ label: sourceState && sourceState.label ? sourceState.label : "Studio editor",
9105
+ path: sourceState && sourceState.path ? sourceState.path : undefined,
9106
+ });
9107
+ if (!sent) {
9108
+ completionSuggestionInFlight = false;
9109
+ completionSuggestionRequestId = null;
9110
+ completionSuggestionPendingSnapshot = null;
9111
+ completionSuggestionRefocusEditorOnResult = false;
9112
+ syncActionButtons();
9113
+ }
9114
+ }
9115
+
9116
+ function insertCompletionSuggestion() {
9117
+ const state = completionSuggestionState;
9118
+ if (!state || typeof state.suggestion !== "string") {
9119
+ setStatus("No suggestion to insert.", "warning");
9120
+ return;
9121
+ }
9122
+ const currentText = String(sourceTextEl.value || "");
9123
+ const useOriginalRange = currentText === state.baseText;
9124
+ const start = useOriginalRange
9125
+ ? Math.max(0, Math.min(state.selectionStart, currentText.length))
9126
+ : (typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : currentText.length);
9127
+ const end = useOriginalRange
9128
+ ? Math.max(start, Math.min(state.selectionEnd, currentText.length))
9129
+ : (typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : start);
9130
+ const nextText = currentText.slice(0, start) + state.suggestion + currentText.slice(end);
9131
+ const caret = start + state.suggestion.length;
9132
+ applySourceTextEdit(nextText, caret, caret);
9133
+ hideCompletionSuggestion();
9134
+ focusSourceTextNoScroll();
9135
+ setStatus("Inserted completion suggestion.", "success");
9136
+ }
9137
+
9138
+ function handleCompletionSuggestionServerMessage(message) {
9139
+ if (!message || typeof message !== "object") return false;
9140
+ if (
9141
+ message.type !== "completion_suggestion_progress"
9142
+ && message.type !== "completion_suggestion_result"
9143
+ && message.type !== "completion_suggestion_error"
9144
+ ) return false;
9145
+ if (typeof message.requestId === "string" && completionSuggestionRequestId && message.requestId !== completionSuggestionRequestId) {
9146
+ return true;
9147
+ }
9148
+ if (message.type === "completion_suggestion_progress") {
9149
+ setStatus(typeof message.message === "string" ? message.message : "Generating suggestion…", "warning");
9150
+ return true;
9151
+ }
9152
+ const pendingSnapshot = completionSuggestionPendingSnapshot;
9153
+ const shouldRefocusEditor = completionSuggestionRefocusEditorOnResult;
9154
+ completionSuggestionInFlight = false;
9155
+ completionSuggestionRequestId = null;
9156
+ completionSuggestionPendingSnapshot = null;
9157
+ completionSuggestionRefocusEditorOnResult = false;
9158
+ syncActionButtons();
9159
+ if (message.type === "completion_suggestion_error") {
9160
+ setStatus(typeof message.message === "string" ? message.message : "Suggestion failed.", "warning");
9161
+ return true;
9162
+ }
9163
+ const suggestion = typeof message.suggestion === "string" ? message.suggestion : "";
9164
+ if (!suggestion.trim()) {
9165
+ setStatus("Model returned an empty suggestion.", "warning");
9166
+ return true;
9167
+ }
9168
+ const text = String(sourceTextEl.value || "");
9169
+ if (pendingSnapshot && text !== pendingSnapshot.text) {
9170
+ setStatus("Editor changed while the suggestion was generating. Please request a fresh suggestion.", "warning");
9171
+ return true;
9172
+ }
9173
+ const baseText = pendingSnapshot ? pendingSnapshot.text : text;
9174
+ const start = Math.max(0, Math.min(pendingSnapshot ? pendingSnapshot.selectionStart : (Number(message.selectionStart) || 0), baseText.length));
9175
+ const end = Math.max(start, Math.min(pendingSnapshot ? pendingSnapshot.selectionEnd : (Number(message.selectionEnd) || start), baseText.length));
9176
+ showCompletionSuggestion({
9177
+ suggestion,
9178
+ baseText,
9179
+ selectionStart: start,
9180
+ selectionEnd: end,
9181
+ });
9182
+ const activeEl = document.activeElement;
9183
+ if (
9184
+ shouldRefocusEditor
9185
+ || activeEl === sourceTextEl
9186
+ || activeEl === suggestCompletionBtn
9187
+ || activeEl === document.body
9188
+ || activeEl === document.documentElement
9189
+ || Boolean(completionSuggestionPanelEl && activeEl instanceof Element && completionSuggestionPanelEl.contains(activeEl))
9190
+ ) {
9191
+ focusSourceEditorForCompletion();
9192
+ }
9193
+ setStatus("Suggestion ready. Press Tab to insert it, or use the Insert suggestion button.", "success");
9194
+ return true;
9195
+ }
9196
+
8704
9197
  function getSourceTextLineEditBounds(text, selectionStart, selectionEnd) {
8705
9198
  const source = String(text || "");
8706
9199
  const safeStart = Math.max(0, Math.min(Math.floor(Number(selectionStart) || 0), source.length));
@@ -8780,7 +9273,14 @@
8780
9273
  }
8781
9274
 
8782
9275
  function handleSourceTextTabKey(event) {
8783
- if (!event || event.key !== "Tab" || event.metaKey || event.ctrlKey || event.altKey) return;
9276
+ if (!event) return;
9277
+ if (isCompletionSuggestionRequestShortcut(event)) {
9278
+ event.preventDefault();
9279
+ requestCompletionSuggestion();
9280
+ return;
9281
+ }
9282
+ if (handleCompletionSuggestionAcceptKey(event)) return;
9283
+ if (event.key !== "Tab" || event.metaKey || event.ctrlKey || event.altKey) return;
8784
9284
  event.preventDefault();
8785
9285
  if (event.shiftKey) {
8786
9286
  unindentSourceTextSelection();
@@ -8833,7 +9333,9 @@
8833
9333
  ? "editor-preview"
8834
9334
  : (nextView === "repl"
8835
9335
  ? "repl"
8836
- : ((nextView === "trace" || nextView === "thinking") ? "trace" : "markdown")));
9336
+ : (nextView === "files"
9337
+ ? "files"
9338
+ : ((nextView === "trace" || nextView === "thinking") ? "trace" : "markdown"))));
8837
9339
  rightViewSelect.value = rightView;
8838
9340
  if (rightView === "trace" && previousView !== "trace") {
8839
9341
  traceAutoScroll = true;
@@ -9173,6 +9675,11 @@
9173
9675
  ".diff", ".patch",
9174
9676
  ]);
9175
9677
  const PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
9678
+ const PREVIEW_LOCAL_TEXT_LINK_FILENAMES = new Set([
9679
+ ".dockerignore", ".editorconfig", ".env", ".env.example", ".eslintignore", ".gitattributes",
9680
+ ".gitignore", ".gitmodules", ".npmignore", ".prettierignore", "dockerfile", "gemfile",
9681
+ "justfile", "license", "makefile", "rakefile", "readme",
9682
+ ]);
9176
9683
  let previewLinkMenuEl = null;
9177
9684
  let activePreviewLinkContext = null;
9178
9685
 
@@ -9220,10 +9727,17 @@
9220
9727
  return match ? ("." + match[1].toLowerCase()) : "";
9221
9728
  }
9222
9729
 
9730
+ function getPreviewLocalLinkFilename(href) {
9731
+ const path = stripPreviewLocalLinkUrlSuffix(href).replace(/\\/g, "/");
9732
+ const parts = path.split("/");
9733
+ return (parts.pop() || "").toLowerCase();
9734
+ }
9735
+
9223
9736
  function getPreviewLocalLinkKind(href) {
9224
9737
  const ext = getPreviewLocalLinkExtension(href);
9738
+ const name = getPreviewLocalLinkFilename(href);
9225
9739
  if (ext === ".pdf") return "pdf";
9226
- if (PREVIEW_LOCAL_TEXT_LINK_EXTENSIONS.has(ext)) return "text";
9740
+ if (PREVIEW_LOCAL_TEXT_LINK_EXTENSIONS.has(ext) || PREVIEW_LOCAL_TEXT_LINK_FILENAMES.has(name)) return "text";
9227
9741
  if (PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS.has(ext)) return "image";
9228
9742
  return "other";
9229
9743
  }
@@ -9240,9 +9754,11 @@
9240
9754
  function getEffectivePreviewLinkContext(contextOverride) {
9241
9755
  const fallback = getHtmlPreviewResourceContextOptions();
9242
9756
  const context = contextOverride && typeof contextOverride === "object" ? contextOverride : null;
9757
+ const hasSourcePath = Boolean(context && Object.prototype.hasOwnProperty.call(context, "sourcePath"));
9758
+ const hasResourceDir = Boolean(context && Object.prototype.hasOwnProperty.call(context, "resourceDir"));
9243
9759
  return {
9244
- sourcePath: context && context.sourcePath ? String(context.sourcePath) : (fallback.sourcePath || ""),
9245
- resourceDir: context && context.resourceDir ? String(context.resourceDir) : (fallback.resourceDir || ""),
9760
+ sourcePath: hasSourcePath ? String(context.sourcePath || "") : (fallback.sourcePath || ""),
9761
+ resourceDir: hasResourceDir ? String(context.resourceDir || "") : (fallback.resourceDir || ""),
9246
9762
  };
9247
9763
  }
9248
9764
 
@@ -15706,6 +16222,8 @@
15706
16222
  updateFooterMeta();
15707
16223
  }
15708
16224
 
16225
+ if (handleCompletionSuggestionServerMessage(message)) return;
16226
+
15709
16227
  if (
15710
16228
  message.type === "quiz_progress" ||
15711
16229
  message.type === "quiz_generated" ||
@@ -16957,6 +17475,9 @@
16957
17475
  sourceTextEl.addEventListener("keydown", handleSourceTextTabKey);
16958
17476
 
16959
17477
  sourceTextEl.addEventListener("input", () => {
17478
+ if (completionSuggestionState && sourceTextEl.value !== completionSuggestionState.baseText) {
17479
+ hideCompletionSuggestion();
17480
+ }
16960
17481
  if (activePreviewCommentSelection) {
16961
17482
  clearPreviewCommentSelection();
16962
17483
  }
@@ -16965,6 +17486,7 @@
16965
17486
  scheduleEditorMetaUpdate();
16966
17487
  updateEditorSelectionCommentUi();
16967
17488
  updateOutlineUi();
17489
+ syncActionButtons();
16968
17490
  if (isReviewNotesOpen() && reviewNotes.length > 0) {
16969
17491
  renderReviewNotesList();
16970
17492
  updateReviewNotesUi();
@@ -17420,6 +17942,24 @@
17420
17942
  }
17421
17943
  });
17422
17944
 
17945
+ if (suggestCompletionBtn) {
17946
+ suggestCompletionBtn.addEventListener("click", () => {
17947
+ requestCompletionSuggestion();
17948
+ });
17949
+ }
17950
+ if (completionSuggestionInsertBtn) {
17951
+ completionSuggestionInsertBtn.addEventListener("click", () => {
17952
+ insertCompletionSuggestion();
17953
+ });
17954
+ }
17955
+ if (completionSuggestionDismissBtn) {
17956
+ completionSuggestionDismissBtn.addEventListener("click", () => {
17957
+ hideCompletionSuggestion();
17958
+ focusSourceTextNoScroll();
17959
+ setStatus("Dismissed completion suggestion.");
17960
+ });
17961
+ }
17962
+
17423
17963
  if (reviewNotesBtn) {
17424
17964
  reviewNotesBtn.addEventListener("click", () => {
17425
17965
  toggleReviewNotes();
@@ -17891,7 +18431,7 @@
17891
18431
  renderSourcePreview();
17892
18432
  workspacePersistenceReady = true;
17893
18433
  if (workspaceRestoredFromBrowser) {
17894
- setStatus("Restored editor workspace from this browser tab. Use Clear editor to discard it.", "success");
18434
+ setStatus("Restored editor workspace from this browser tab. Use Reset editor to discard it.", "success");
17895
18435
  }
17896
18436
  connect();
17897
18437
  } catch (error) {