pi-studio 0.9.14 → 0.9.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,20 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.15] — 2026-05-23
8
+
9
+ ### Added
10
+ - Added a first-cut right-pane **Files** view for browsing the current Studio resource directory, opening folders, opening text/code documents in the editor, previewing local PDFs/images, copying paths, and revealing files in the file manager.
11
+ - Added text-file recognition for extensionless/dotfile project files such as `.gitignore`, `.gitattributes`, `.env`, `Dockerfile`, `Makefile`, `Justfile`, `LICENSE`, and `README`.
12
+
13
+ ### Changed
14
+ - The Files view now treats the Pi session directory/resource directory as its navigation root, while opening initially near the active file when applicable.
15
+ - Renamed **Clear editor** to **Reset editor** and clarified that it resets the tab to a fresh blank draft without changing saved files or responses.
16
+ - Browser-tab workspace restoration now uses per-tab session storage instead of shared local storage.
17
+
18
+ ### Fixed
19
+ - Fixed editor-only companion tabs inheriting a Files right-pane state from another Studio tab after opening a document in a new editor.
20
+
7
21
  ## [0.9.14] — 2026-05-22
8
22
 
9
23
  ### Added
@@ -19,7 +33,7 @@ All notable changes to `pi-studio` are documented here.
19
33
  ## [0.9.13] — 2026-05-22
20
34
 
21
35
  ### Added
22
- - Added browser-refresh restoration for the Studio editor workspace, plus an explicit **Clear editor** action for discarding the restored browser draft without touching saved files or response history.
36
+ - Added browser-refresh restoration for the Studio editor workspace, plus an explicit **Reset editor** action for discarding the restored browser draft without touching saved files or response history.
23
37
  - Added local preview-link handling for rendered document previews and sandboxed HTML previews: local PDF links open in Studio's embedded PDF focus viewer, local image links open in a local image preview, text/code/document links can open in a new editor tab, and right-clicking local links offers **Open here**, **Reveal in file manager**, and **Copy path** where applicable.
24
38
  - Brought editor-only Studio closer to full Studio for document-local controls: pane resizing/focus shortcuts now work, and the annotation-header toggle remains available.
25
39
 
package/README.md CHANGED
@@ -23,10 +23,11 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
23
23
  - Includes a global **Zen** mode for hiding secondary Studio chrome without changing the current left/right pane layout
24
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
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
+ - 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
26
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
27
28
  - Includes a local persistent scratchpad for quick notes you want to keep out of the main editor until you're ready to copy or insert them
28
29
  - Includes a docked **Outline** rail for navigating document structure in the current editor text, with clickable entries that jump in the raw editor and reveal matching preview locations when available
29
- - Restores the current browser-tab editor workspace after refresh and provides an explicit **Clear editor** action when you want to discard the restored draft without changing responses or saved files
30
+ - Restores the current browser-tab editor workspace after refresh and provides an explicit **Reset editor** action when you want to discard the restored draft and return the tab to a fresh blank draft without changing responses or saved files
30
31
  - Turns local preview links, including links inside sandboxed HTML previews, into Studio actions: PDFs open in the embedded viewer, text/code/document links can open in a new editor tab, and right-click menus provide **Open here**, **Reveal in file manager**, and **Copy path** for local resources
31
32
  - Includes local comments anchored to selections/lines, shown in a docked **Comments** rail, with transient **Comment** / **Jump** actions from raw-editor selections plus editor-preview selections for Markdown, LaTeX, and code/text/diff previews, alongside optional inline `[an: ...]` toggles when you want comments reflected in the document text
32
33
  - Browses response history (`Prev/Next/Last`) and loads either:
@@ -1955,6 +1955,19 @@
1955
1955
  const DEFAULT_RESPONSE_FONT_SIZE = studioUiRefreshEnabled ? 13.5 : 15;
1956
1956
  let editorFontSize = DEFAULT_EDITOR_FONT_SIZE;
1957
1957
  let responseFontSize = DEFAULT_RESPONSE_FONT_SIZE;
1958
+ let fileBrowserState = {
1959
+ rootDir: "",
1960
+ currentDir: "",
1961
+ relativeDir: "",
1962
+ parentDir: null,
1963
+ entries: [],
1964
+ omitted: 0,
1965
+ omittedIgnored: 0,
1966
+ loading: false,
1967
+ error: "",
1968
+ loaded: false,
1969
+ };
1970
+ let fileBrowserLoadNonce = 0;
1958
1971
  let studioUiRefreshUi = null;
1959
1972
  let studioZenModeEnabled = readStudioZenModeEnabled();
1960
1973
  if (studioUiRefreshEnabled && document.body) {
@@ -4085,6 +4098,12 @@
4085
4098
  }
4086
4099
  if (referenceMetaEl instanceof HTMLElement) referenceMetaEl.hidden = false;
4087
4100
 
4101
+ if (rightView === "files") {
4102
+ const dir = fileBrowserState && fileBrowserState.currentDir ? fileBrowserState.currentDir : (getCurrentResourceDirValue() || "current Studio directory");
4103
+ referenceBadgeEl.textContent = "Files: " + dir;
4104
+ return;
4105
+ }
4106
+
4088
4107
  if (rightView === "trace") {
4089
4108
  const state = traceState || createEmptyTraceState();
4090
4109
  const context = traceDisplayContext || {};
@@ -6645,6 +6664,7 @@
6645
6664
  critiqueViewEl.addEventListener("scroll", handleTracePaneScroll);
6646
6665
  critiqueViewEl.addEventListener("click", handleTracePaneClick);
6647
6666
  critiqueViewEl.addEventListener("click", handleReplPaneClick);
6667
+ critiqueViewEl.addEventListener("click", handleFilesPaneClick);
6648
6668
  critiqueViewEl.addEventListener("change", handleReplPaneChange);
6649
6669
  }
6650
6670
 
@@ -8107,6 +8127,241 @@
8107
8127
  scheduleResponsePaneRepaintNudge();
8108
8128
  }
8109
8129
 
8130
+ function getFileBrowserContextKey() {
8131
+ const context = getHtmlPreviewResourceContextOptions();
8132
+ return String(context.sourcePath || "") + "\n" + String(context.resourceDir || "");
8133
+ }
8134
+
8135
+ function getFileBrowserLocalLinkContext() {
8136
+ return { sourcePath: "", resourceDir: fileBrowserState.rootDir || getCurrentResourceDirValue() || "" };
8137
+ }
8138
+
8139
+ function formatFileBrowserSize(size) {
8140
+ const value = Number(size);
8141
+ if (!Number.isFinite(value) || value < 0) return "";
8142
+ if (value < 1024) return Math.round(value) + " B";
8143
+ if (value < 1024 * 1024) return (value / 1024).toFixed(value < 10 * 1024 ? 1 : 0) + " KB";
8144
+ if (value < 1024 * 1024 * 1024) return (value / (1024 * 1024)).toFixed(value < 10 * 1024 * 1024 ? 1 : 0) + " MB";
8145
+ return (value / (1024 * 1024 * 1024)).toFixed(1) + " GB";
8146
+ }
8147
+
8148
+ function formatFileBrowserTime(ms) {
8149
+ const value = Number(ms);
8150
+ if (!Number.isFinite(value) || value <= 0) return "";
8151
+ try {
8152
+ return new Date(value).toLocaleDateString([], { month: "short", day: "numeric", year: "numeric" });
8153
+ } catch {
8154
+ return "";
8155
+ }
8156
+ }
8157
+
8158
+ function getFileBrowserKindLabel(entry) {
8159
+ if (!entry || entry.type === "directory") return "folder";
8160
+ if (entry.kind === "text") return "document";
8161
+ if (entry.kind === "pdf") return "PDF";
8162
+ if (entry.kind === "image") return "image";
8163
+ return entry.extension ? entry.extension.replace(/^\./, "") : "file";
8164
+ }
8165
+
8166
+ function buildFileBrowserPanelHtml() {
8167
+ const state = fileBrowserState || {};
8168
+ const entries = Array.isArray(state.entries) ? state.entries : [];
8169
+ const currentDir = state.currentDir || "";
8170
+ const rootDir = state.rootDir || "";
8171
+ const relativeDir = state.relativeDir || ".";
8172
+ const parentDisabled = state.parentDir ? "" : " disabled";
8173
+ const rows = entries.length
8174
+ ? entries.map((entry) => {
8175
+ const type = entry.type === "directory" ? "directory" : "file";
8176
+ const kind = entry.kind || (type === "directory" ? "directory" : "other");
8177
+ const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" ? "📝" : "📦")));
8178
+ const metaParts = [];
8179
+ metaParts.push(getFileBrowserKindLabel(entry));
8180
+ if (type === "file") metaParts.push(formatFileBrowserSize(entry.size));
8181
+ const time = formatFileBrowserTime(entry.mtimeMs);
8182
+ if (time) metaParts.push(time);
8183
+ const textActions = kind === "text"
8184
+ ? "<button type='button' data-files-action='open-new' data-files-path='" + escapeHtml(entry.path) + "'>New tab</button>"
8185
+ : "";
8186
+ const openTitle = type === "directory"
8187
+ ? "Open folder"
8188
+ : (kind === "text" ? "Open in editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file")));
8189
+ return "<div class='files-row files-row-" + escapeHtml(type) + " files-kind-" + escapeHtml(kind) + "'>"
8190
+ + "<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) + "'>"
8191
+ + "<span class='files-icon' aria-hidden='true'>" + icon + "</span>"
8192
+ + "<span class='files-name'>" + escapeHtml(entry.name) + "</span>"
8193
+ + "<span class='files-meta'>" + escapeHtml(metaParts.filter(Boolean).join(" · ")) + "</span>"
8194
+ + "</button>"
8195
+ + "<span class='files-actions'>"
8196
+ + textActions
8197
+ + "<button type='button' data-files-action='copy-path' data-files-path='" + escapeHtml(entry.path) + "'>Copy path</button>"
8198
+ + (type === "file" ? "<button type='button' data-files-action='reveal' data-files-path='" + escapeHtml(entry.path) + "'>Reveal</button>" : "")
8199
+ + "</span>"
8200
+ + "</div>";
8201
+ }).join("")
8202
+ : "<div class='files-empty'>" + (state.loading ? "Loading files…" : "This folder is empty.") + "</div>";
8203
+ const notices = [];
8204
+ if (state.error) notices.push("<div class='files-notice files-notice-error'>" + escapeHtml(state.error) + "</div>");
8205
+ if (state.omitted) notices.push("<div class='files-notice'>" + escapeHtml(String(state.omitted)) + " item" + (state.omitted === 1 ? "" : "s") + " omitted.</div>");
8206
+ if (state.omittedIgnored) notices.push("<div class='files-notice'>" + escapeHtml(String(state.omittedIgnored)) + " heavy/cache folder" + (state.omittedIgnored === 1 ? "" : "s") + " hidden.</div>");
8207
+ return "<div class='files-panel'>"
8208
+ + "<div class='files-toolbar'>"
8209
+ + "<div class='files-path-group'><span class='files-label'>Files</span><span class='files-path' title='" + escapeHtml(currentDir) + "'>" + escapeHtml(relativeDir || ".") + "</span></div>"
8210
+ + "<div class='files-toolbar-actions'>"
8211
+ + "<button type='button' data-files-action='parent'" + parentDisabled + ">Parent</button>"
8212
+ + "<button type='button' data-files-action='refresh'>Refresh</button>"
8213
+ + (rootDir ? "<button type='button' data-files-action='copy-root' data-files-path='" + escapeHtml(rootDir) + "'>Copy root</button>" : "")
8214
+ + "</div>"
8215
+ + "</div>"
8216
+ + "<div class='files-subtitle'>Root: <span title='" + escapeHtml(rootDir) + "'>" + escapeHtml(rootDir || "current Studio directory") + "</span></div>"
8217
+ + notices.join("")
8218
+ + "<div class='files-list' role='list'>" + rows + "</div>"
8219
+ + "</div>";
8220
+ }
8221
+
8222
+ function renderFilesView() {
8223
+ if (!critiqueViewEl) return;
8224
+ const contextKey = getFileBrowserContextKey();
8225
+ if (fileBrowserState.contextKey !== contextKey) {
8226
+ fileBrowserState = {
8227
+ rootDir: "",
8228
+ currentDir: "",
8229
+ relativeDir: "",
8230
+ parentDir: null,
8231
+ entries: [],
8232
+ omitted: 0,
8233
+ omittedIgnored: 0,
8234
+ loading: false,
8235
+ error: "",
8236
+ loaded: false,
8237
+ contextKey,
8238
+ };
8239
+ }
8240
+ finishPreviewRender(critiqueViewEl);
8241
+ critiqueViewEl.innerHTML = buildFileBrowserPanelHtml();
8242
+ critiqueViewEl.classList.remove("response-scroll-resetting");
8243
+ if (!fileBrowserState.loaded && !fileBrowserState.loading) {
8244
+ loadFileBrowserDirectory("");
8245
+ }
8246
+ scheduleResponsePaneRepaintNudge();
8247
+ }
8248
+
8249
+ async function loadFileBrowserDirectory(dir, options) {
8250
+ const context = getHtmlPreviewResourceContextOptions();
8251
+ const contextKey = getFileBrowserContextKey();
8252
+ const nonce = ++fileBrowserLoadNonce;
8253
+ fileBrowserState = {
8254
+ ...fileBrowserState,
8255
+ contextKey,
8256
+ loading: true,
8257
+ error: "",
8258
+ };
8259
+ if (rightView === "files") {
8260
+ finishPreviewRender(critiqueViewEl);
8261
+ critiqueViewEl.innerHTML = buildFileBrowserPanelHtml();
8262
+ }
8263
+ try {
8264
+ const query = {};
8265
+ if (dir) query.dir = String(dir);
8266
+ if (context.sourcePath) query.sourcePath = context.sourcePath;
8267
+ if (context.resourceDir) query.resourceDir = context.resourceDir;
8268
+ const payload = await fetchStudioJson("/file-browser", { query });
8269
+ if (nonce !== fileBrowserLoadNonce) return;
8270
+ fileBrowserState = {
8271
+ rootDir: typeof payload.rootDir === "string" ? payload.rootDir : "",
8272
+ currentDir: typeof payload.currentDir === "string" ? payload.currentDir : "",
8273
+ relativeDir: typeof payload.relativeDir === "string" ? payload.relativeDir : ".",
8274
+ parentDir: typeof payload.parentDir === "string" ? payload.parentDir : null,
8275
+ entries: Array.isArray(payload.entries) ? payload.entries : [],
8276
+ omitted: Number(payload.omitted) || 0,
8277
+ omittedIgnored: Number(payload.omittedIgnored) || 0,
8278
+ loading: false,
8279
+ error: "",
8280
+ loaded: true,
8281
+ contextKey,
8282
+ };
8283
+ if (rightView === "files") {
8284
+ finishPreviewRender(critiqueViewEl);
8285
+ critiqueViewEl.innerHTML = buildFileBrowserPanelHtml();
8286
+ scheduleResponsePaneRepaintNudge();
8287
+ }
8288
+ if (options && options.user) setStatus("Loaded file list.", "success");
8289
+ } catch (error) {
8290
+ if (nonce !== fileBrowserLoadNonce) return;
8291
+ fileBrowserState = {
8292
+ ...fileBrowserState,
8293
+ loading: false,
8294
+ error: (error && error.message) ? error.message : String(error || "Could not load files."),
8295
+ loaded: true,
8296
+ };
8297
+ if (rightView === "files") {
8298
+ finishPreviewRender(critiqueViewEl);
8299
+ critiqueViewEl.innerHTML = buildFileBrowserPanelHtml();
8300
+ scheduleResponsePaneRepaintNudge();
8301
+ }
8302
+ }
8303
+ }
8304
+
8305
+ async function openFileBrowserEntry(path, kind) {
8306
+ const context = getFileBrowserLocalLinkContext();
8307
+ if (kind === "text") {
8308
+ await openPreviewDocumentHere(path, context);
8309
+ return;
8310
+ }
8311
+ if (kind === "pdf") {
8312
+ openPreviewPdfLink(path, path, context);
8313
+ return;
8314
+ }
8315
+ if (kind === "image") {
8316
+ await openPreviewImageLink(path, path, context);
8317
+ return;
8318
+ }
8319
+ setStatus("No Studio preview for this file type. Use Copy path or Reveal.", "warning");
8320
+ }
8321
+
8322
+ async function handleFilesPaneClick(event) {
8323
+ if (rightView !== "files") return;
8324
+ const target = event.target;
8325
+ const actionEl = target instanceof Element ? target.closest("[data-files-action]") : null;
8326
+ if (!actionEl) return;
8327
+ event.preventDefault();
8328
+ const action = actionEl.getAttribute("data-files-action") || "";
8329
+ const path = actionEl.getAttribute("data-files-path") || "";
8330
+ const kind = actionEl.getAttribute("data-files-kind") || getPreviewLocalLinkKind(path);
8331
+ try {
8332
+ if (action === "parent") {
8333
+ if (fileBrowserState.parentDir) await loadFileBrowserDirectory(fileBrowserState.parentDir, { user: true });
8334
+ return;
8335
+ }
8336
+ if (action === "refresh") {
8337
+ await loadFileBrowserDirectory(fileBrowserState.currentDir || "", { user: true });
8338
+ return;
8339
+ }
8340
+ if (action === "open-dir") {
8341
+ await loadFileBrowserDirectory(path, { user: true });
8342
+ return;
8343
+ }
8344
+ if (action === "open") {
8345
+ await openFileBrowserEntry(path, kind);
8346
+ return;
8347
+ }
8348
+ if (action === "open-new") {
8349
+ await openPreviewDocumentInNewEditor(path, null, getFileBrowserLocalLinkContext());
8350
+ return;
8351
+ }
8352
+ if (action === "copy-path" || action === "copy-root") {
8353
+ const ok = await writeTextToClipboard(path);
8354
+ setStatus(ok ? "Copied path." : "Clipboard write failed.", ok ? "success" : "warning");
8355
+ return;
8356
+ }
8357
+ if (action === "reveal") {
8358
+ await revealPreviewLocalLink(path, getFileBrowserLocalLinkContext());
8359
+ }
8360
+ } catch (error) {
8361
+ setStatus((error && error.message) ? error.message : String(error || "File action failed."), "warning");
8362
+ }
8363
+ }
8364
+
8110
8365
  function renderActiveResult() {
8111
8366
  if (rightView === "trace") {
8112
8367
  renderTraceView();
@@ -8118,6 +8373,11 @@
8118
8373
  return;
8119
8374
  }
8120
8375
 
8376
+ if (rightView === "files") {
8377
+ renderFilesView();
8378
+ return;
8379
+ }
8380
+
8121
8381
  if (rightView === "editor-preview") {
8122
8382
  const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
8123
8383
  if (!editorText.trim()) {
@@ -8192,7 +8452,7 @@
8192
8452
  : normalizeForCompare(sourceTextEl.value);
8193
8453
  const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
8194
8454
  const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
8195
- const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl";
8455
+ const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl" || rightView === "files";
8196
8456
 
8197
8457
  if (responseWrapEl) {
8198
8458
  responseWrapEl.hidden = showingAuxiliaryRightPane;
@@ -8233,6 +8493,8 @@
8233
8493
  : (exportingReplJournal ? "Export record" : "Export right preview");
8234
8494
  if (rightView === "trace") {
8235
8495
  exportPdfBtn.title = "Working view does not support preview export.";
8496
+ } else if (rightView === "files") {
8497
+ exportPdfBtn.title = "Files view does not support preview export.";
8236
8498
  } else if (exportingReplJournal && !replJournalExportEntries.length) {
8237
8499
  exportPdfBtn.title = "No Studio REPL record entries to export for this session yet.";
8238
8500
  } else if (rightView === "markdown") {
@@ -8465,9 +8727,24 @@
8465
8727
  return "source:" + normalized.source + ":" + normalized.label;
8466
8728
  }
8467
8729
 
8730
+ function getWorkspacePersistenceStorage() {
8731
+ try {
8732
+ return window.sessionStorage || null;
8733
+ } catch {
8734
+ return null;
8735
+ }
8736
+ }
8737
+
8738
+ function clearLegacyWorkspacePersistenceStorage() {
8739
+ try {
8740
+ if (window.localStorage) window.localStorage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
8741
+ } catch {}
8742
+ }
8743
+
8468
8744
  function readPersistedWorkspaceState() {
8469
8745
  try {
8470
- const raw = window.localStorage ? window.localStorage.getItem(STUDIO_WORKSPACE_STORAGE_KEY) : null;
8746
+ const storage = getWorkspacePersistenceStorage();
8747
+ const raw = storage ? storage.getItem(STUDIO_WORKSPACE_STORAGE_KEY) : null;
8471
8748
  if (!raw) return null;
8472
8749
  const parsed = JSON.parse(raw);
8473
8750
  if (!parsed || typeof parsed !== "object" || parsed.version !== 1) return null;
@@ -8495,7 +8772,7 @@
8495
8772
  sourceState: normalizeWorkspaceSourceState(sourceState),
8496
8773
  resourceDir: getCurrentResourceDirValue(),
8497
8774
  editorView,
8498
- rightView,
8775
+ rightView: isEditorOnlyMode ? "editor-preview" : rightView,
8499
8776
  editorLanguage,
8500
8777
  followLatest,
8501
8778
  responseHistoryIndex,
@@ -8509,13 +8786,15 @@
8509
8786
  function persistWorkspaceStateNow() {
8510
8787
  if (!workspacePersistenceReady) return;
8511
8788
  try {
8512
- if (!window.localStorage) return;
8789
+ const storage = getWorkspacePersistenceStorage();
8790
+ if (!storage) return;
8791
+ clearLegacyWorkspacePersistenceStorage();
8513
8792
  const payload = buildWorkspacePersistencePayload();
8514
8793
  if (payload.text.length > STUDIO_WORKSPACE_MAX_TEXT_CHARS) {
8515
- window.localStorage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
8794
+ storage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
8516
8795
  return;
8517
8796
  }
8518
- window.localStorage.setItem(STUDIO_WORKSPACE_STORAGE_KEY, JSON.stringify(payload));
8797
+ storage.setItem(STUDIO_WORKSPACE_STORAGE_KEY, JSON.stringify(payload));
8519
8798
  } catch {
8520
8799
  // Ignore browser storage failures and quota limits.
8521
8800
  }
@@ -8544,8 +8823,10 @@
8544
8823
  workspacePersistTimer = null;
8545
8824
  }
8546
8825
  try {
8547
- if (window.localStorage) window.localStorage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
8826
+ const storage = getWorkspacePersistenceStorage();
8827
+ if (storage) storage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
8548
8828
  } catch {}
8829
+ clearLegacyWorkspacePersistenceStorage();
8549
8830
  }
8550
8831
 
8551
8832
  function applyPersistedWorkspaceState(state) {
@@ -8563,11 +8844,15 @@
8563
8844
  setEditorLanguage(state.editorLanguage.trim());
8564
8845
  }
8565
8846
  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")));
8847
+ rightView = isEditorOnlyMode
8848
+ ? "editor-preview"
8849
+ : (state.rightView === "preview"
8850
+ ? "preview"
8851
+ : (state.rightView === "editor-preview"
8852
+ ? "editor-preview"
8853
+ : (state.rightView === "repl"
8854
+ ? "repl"
8855
+ : (state.rightView === "files" ? "files" : ((state.rightView === "trace" || state.rightView === "thinking") ? "trace" : "markdown")))));
8571
8856
  if (typeof state.followLatest === "boolean") {
8572
8857
  followLatest = state.followLatest;
8573
8858
  }
@@ -8592,7 +8877,7 @@
8592
8877
  setStatus("Studio is busy.", "warning");
8593
8878
  return;
8594
8879
  }
8595
- const confirmed = window.confirm("Clear the current editor draft in this browser tab? Saved files and responses are not changed.");
8880
+ const confirmed = window.confirm("Reset the editor to a fresh blank draft in this browser tab? Saved files and responses are not changed.");
8596
8881
  if (!confirmed) return;
8597
8882
  const preservedResponseState = {
8598
8883
  responseHistory: Array.isArray(responseHistory) ? responseHistory.slice() : [],
@@ -8634,7 +8919,7 @@
8634
8919
  if (followSelect) followSelect.value = followLatest ? "on" : "off";
8635
8920
  refreshResponseUi();
8636
8921
  persistWorkspaceStateNow();
8637
- setStatus("Editor cleared. Saved files and responses were not changed.", "success");
8922
+ setStatus("Editor reset to a fresh blank draft. Saved files and responses were not changed.", "success");
8638
8923
  }
8639
8924
 
8640
8925
  function setEditorText(nextText, options) {
@@ -8833,7 +9118,9 @@
8833
9118
  ? "editor-preview"
8834
9119
  : (nextView === "repl"
8835
9120
  ? "repl"
8836
- : ((nextView === "trace" || nextView === "thinking") ? "trace" : "markdown")));
9121
+ : (nextView === "files"
9122
+ ? "files"
9123
+ : ((nextView === "trace" || nextView === "thinking") ? "trace" : "markdown"))));
8837
9124
  rightViewSelect.value = rightView;
8838
9125
  if (rightView === "trace" && previousView !== "trace") {
8839
9126
  traceAutoScroll = true;
@@ -9173,6 +9460,11 @@
9173
9460
  ".diff", ".patch",
9174
9461
  ]);
9175
9462
  const PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
9463
+ const PREVIEW_LOCAL_TEXT_LINK_FILENAMES = new Set([
9464
+ ".dockerignore", ".editorconfig", ".env", ".env.example", ".eslintignore", ".gitattributes",
9465
+ ".gitignore", ".gitmodules", ".npmignore", ".prettierignore", "dockerfile", "gemfile",
9466
+ "justfile", "license", "makefile", "rakefile", "readme",
9467
+ ]);
9176
9468
  let previewLinkMenuEl = null;
9177
9469
  let activePreviewLinkContext = null;
9178
9470
 
@@ -9220,10 +9512,17 @@
9220
9512
  return match ? ("." + match[1].toLowerCase()) : "";
9221
9513
  }
9222
9514
 
9515
+ function getPreviewLocalLinkFilename(href) {
9516
+ const path = stripPreviewLocalLinkUrlSuffix(href).replace(/\\/g, "/");
9517
+ const parts = path.split("/");
9518
+ return (parts.pop() || "").toLowerCase();
9519
+ }
9520
+
9223
9521
  function getPreviewLocalLinkKind(href) {
9224
9522
  const ext = getPreviewLocalLinkExtension(href);
9523
+ const name = getPreviewLocalLinkFilename(href);
9225
9524
  if (ext === ".pdf") return "pdf";
9226
- if (PREVIEW_LOCAL_TEXT_LINK_EXTENSIONS.has(ext)) return "text";
9525
+ if (PREVIEW_LOCAL_TEXT_LINK_EXTENSIONS.has(ext) || PREVIEW_LOCAL_TEXT_LINK_FILENAMES.has(name)) return "text";
9227
9526
  if (PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS.has(ext)) return "image";
9228
9527
  return "other";
9229
9528
  }
@@ -9240,9 +9539,11 @@
9240
9539
  function getEffectivePreviewLinkContext(contextOverride) {
9241
9540
  const fallback = getHtmlPreviewResourceContextOptions();
9242
9541
  const context = contextOverride && typeof contextOverride === "object" ? contextOverride : null;
9542
+ const hasSourcePath = Boolean(context && Object.prototype.hasOwnProperty.call(context, "sourcePath"));
9543
+ const hasResourceDir = Boolean(context && Object.prototype.hasOwnProperty.call(context, "resourceDir"));
9243
9544
  return {
9244
- sourcePath: context && context.sourcePath ? String(context.sourcePath) : (fallback.sourcePath || ""),
9245
- resourceDir: context && context.resourceDir ? String(context.resourceDir) : (fallback.resourceDir || ""),
9545
+ sourcePath: hasSourcePath ? String(context.sourcePath || "") : (fallback.sourcePath || ""),
9546
+ resourceDir: hasResourceDir ? String(context.resourceDir || "") : (fallback.resourceDir || ""),
9246
9547
  };
9247
9548
  }
9248
9549
 
@@ -17891,7 +18192,7 @@
17891
18192
  renderSourcePreview();
17892
18193
  workspacePersistenceReady = true;
17893
18194
  if (workspaceRestoredFromBrowser) {
17894
- setStatus("Restored editor workspace from this browser tab. Use Clear editor to discard it.", "success");
18195
+ setStatus("Restored editor workspace from this browser tab. Use Reset editor to discard it.", "success");
17895
18196
  }
17896
18197
  connect();
17897
18198
  } catch (error) {
package/client/studio.css CHANGED
@@ -2666,6 +2666,148 @@
2666
2666
  flex: 0 0 auto;
2667
2667
  }
2668
2668
 
2669
+ .files-panel {
2670
+ display: flex;
2671
+ flex-direction: column;
2672
+ gap: 10px;
2673
+ color: var(--text);
2674
+ font-family: var(--font-ui);
2675
+ font-size: var(--studio-response-font-size);
2676
+ }
2677
+
2678
+ .files-toolbar {
2679
+ display: flex;
2680
+ align-items: center;
2681
+ justify-content: space-between;
2682
+ gap: 10px;
2683
+ flex-wrap: wrap;
2684
+ padding: 10px;
2685
+ border: 1px solid var(--border-subtle);
2686
+ border-radius: 10px;
2687
+ background: var(--panel-2);
2688
+ }
2689
+
2690
+ .files-path-group {
2691
+ min-width: 0;
2692
+ display: flex;
2693
+ align-items: baseline;
2694
+ gap: 8px;
2695
+ }
2696
+
2697
+ .files-label {
2698
+ flex: 0 0 auto;
2699
+ font-size: 11px;
2700
+ font-weight: 700;
2701
+ letter-spacing: 0.04em;
2702
+ text-transform: uppercase;
2703
+ color: var(--studio-info-text, var(--muted));
2704
+ }
2705
+
2706
+ .files-path,
2707
+ .files-subtitle span {
2708
+ min-width: 0;
2709
+ overflow: hidden;
2710
+ text-overflow: ellipsis;
2711
+ white-space: nowrap;
2712
+ font-family: var(--font-mono);
2713
+ }
2714
+
2715
+ .files-toolbar-actions,
2716
+ .files-actions {
2717
+ display: inline-flex;
2718
+ align-items: center;
2719
+ gap: 6px;
2720
+ flex-wrap: wrap;
2721
+ }
2722
+
2723
+ .files-toolbar-actions button,
2724
+ .files-actions button {
2725
+ padding: 4px 7px;
2726
+ font-size: 11px;
2727
+ }
2728
+
2729
+ .files-subtitle,
2730
+ .files-notice,
2731
+ .files-empty {
2732
+ color: var(--studio-info-text, var(--muted));
2733
+ font-size: 12px;
2734
+ }
2735
+
2736
+ .files-notice-error {
2737
+ color: var(--danger);
2738
+ }
2739
+
2740
+ .files-list {
2741
+ display: grid;
2742
+ gap: 4px;
2743
+ }
2744
+
2745
+ .files-row {
2746
+ display: grid;
2747
+ grid-template-columns: minmax(0, 1fr) auto;
2748
+ align-items: center;
2749
+ gap: 8px;
2750
+ padding: 5px 7px;
2751
+ border: 1px solid var(--border-subtle);
2752
+ border-radius: 9px;
2753
+ background: var(--panel);
2754
+ }
2755
+
2756
+ .files-row:hover {
2757
+ border-color: var(--control-border);
2758
+ background: var(--panel-2);
2759
+ }
2760
+
2761
+ .files-open-btn {
2762
+ display: grid;
2763
+ grid-template-columns: auto minmax(0, 1fr) auto;
2764
+ align-items: center;
2765
+ gap: 8px;
2766
+ min-width: 0;
2767
+ padding: 3px 0;
2768
+ border: 0;
2769
+ background: transparent;
2770
+ color: var(--text);
2771
+ text-align: left;
2772
+ cursor: pointer;
2773
+ }
2774
+
2775
+ .files-open-btn:hover,
2776
+ .files-open-btn:focus-visible {
2777
+ outline: none;
2778
+ color: var(--accent);
2779
+ }
2780
+
2781
+ .files-icon {
2782
+ width: 1.3em;
2783
+ text-align: center;
2784
+ }
2785
+
2786
+ .files-name {
2787
+ min-width: 0;
2788
+ overflow: hidden;
2789
+ text-overflow: ellipsis;
2790
+ white-space: nowrap;
2791
+ font-weight: 550;
2792
+ }
2793
+
2794
+ .files-meta {
2795
+ color: var(--studio-info-text, var(--muted));
2796
+ font-size: 11px;
2797
+ white-space: nowrap;
2798
+ }
2799
+
2800
+ @container (max-width: 620px) {
2801
+ .files-row,
2802
+ .files-open-btn {
2803
+ grid-template-columns: minmax(0, 1fr);
2804
+ }
2805
+ .files-actions,
2806
+ .files-meta {
2807
+ justify-self: start;
2808
+ }
2809
+ }
2810
+
2669
2811
  .trace-panel {
2670
2812
  display: flex;
2671
2813
  flex-direction: column;
package/index.ts CHANGED
@@ -2442,6 +2442,16 @@ const STUDIO_LOCAL_LINK_TEXT_EXTENSIONS = new Set([
2442
2442
  ".diff", ".patch",
2443
2443
  ]);
2444
2444
  const STUDIO_LOCAL_LINK_IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp"]);
2445
+ const STUDIO_LOCAL_LINK_TEXT_FILENAMES = new Set([
2446
+ ".dockerignore", ".editorconfig", ".env", ".env.example", ".eslintignore", ".gitattributes",
2447
+ ".gitignore", ".gitmodules", ".npmignore", ".prettierignore", "dockerfile", "gemfile",
2448
+ "justfile", "license", "makefile", "rakefile", "readme",
2449
+ ]);
2450
+ const STUDIO_FILE_BROWSER_MAX_ENTRIES = 500;
2451
+ const STUDIO_FILE_BROWSER_IGNORED_DIRS = new Set([
2452
+ ".git", "node_modules", ".next", ".cache", "dist", "build", "coverage", "target",
2453
+ "__pycache__", ".venv", "venv", ".mypy_cache", ".pytest_cache", ".ruff_cache",
2454
+ ]);
2445
2455
 
2446
2456
  type StudioLocalPreviewResourceKind = "pdf" | "text" | "image" | "other";
2447
2457
 
@@ -2454,6 +2464,17 @@ interface StudioLocalPreviewResource {
2454
2464
  resourceDir: string;
2455
2465
  }
2456
2466
 
2467
+ interface StudioFileBrowserEntry {
2468
+ name: string;
2469
+ path: string;
2470
+ type: "directory" | "file";
2471
+ extension: string;
2472
+ kind: StudioLocalPreviewResourceKind | "directory";
2473
+ size: number;
2474
+ mtimeMs: number;
2475
+ hidden: boolean;
2476
+ }
2477
+
2457
2478
  function resolveStudioPdfResourcePath(pdfPath: string | undefined, sourcePath: string | undefined, resourceDir: string | undefined, fallbackCwd: string): string {
2458
2479
  const rawPath = typeof pdfPath === "string" ? pdfPath.trim() : "";
2459
2480
  if (!rawPath) throw new Error("Missing PDF path.");
@@ -2522,10 +2543,11 @@ function parseStudioLocalPreviewResourcePage(resourcePath: string): number | nul
2522
2543
  return null;
2523
2544
  }
2524
2545
 
2525
- function getStudioLocalPreviewResourceKind(extension: string): StudioLocalPreviewResourceKind {
2546
+ function getStudioLocalPreviewResourceKind(extension: string, filePathOrName?: string): StudioLocalPreviewResourceKind {
2526
2547
  const ext = extension.toLowerCase();
2548
+ const name = basename(String(filePathOrName || "")).toLowerCase();
2527
2549
  if (ext === ".pdf") return "pdf";
2528
- if (STUDIO_LOCAL_LINK_TEXT_EXTENSIONS.has(ext)) return "text";
2550
+ if (STUDIO_LOCAL_LINK_TEXT_EXTENSIONS.has(ext) || STUDIO_LOCAL_LINK_TEXT_FILENAMES.has(name)) return "text";
2529
2551
  if (STUDIO_LOCAL_LINK_IMAGE_EXTENSIONS.has(ext)) return "image";
2530
2552
  return "other";
2531
2553
  }
@@ -2563,12 +2585,94 @@ function resolveStudioLocalPreviewResourcePath(
2563
2585
  filePath: candidateReal,
2564
2586
  label: rel && rel !== "" ? rel : basename(candidateReal),
2565
2587
  extension,
2566
- kind: getStudioLocalPreviewResourceKind(extension),
2588
+ kind: getStudioLocalPreviewResourceKind(extension, candidateReal),
2567
2589
  page: parseStudioLocalPreviewResourcePage(rawPath),
2568
2590
  resourceDir: boundaryReal,
2569
2591
  };
2570
2592
  }
2571
2593
 
2594
+ function resolveStudioFileBrowserDirectory(
2595
+ dirPath: string | undefined,
2596
+ sourcePath: string | undefined,
2597
+ resourceDir: string | undefined,
2598
+ fallbackCwd: string,
2599
+ ): { rootDir: string; currentDir: string; relativeDir: string; parentDir: string | null } {
2600
+ const context = resolveStudioPreviewResourceContext(sourcePath, resourceDir, fallbackCwd);
2601
+ const rootReal = realpathSync(context.boundaryDir);
2602
+ const rawDir = typeof dirPath === "string" ? dirPath.trim() : "";
2603
+ const baseDir = context.baseDir;
2604
+ const requested = rawDir
2605
+ ? (isAbsolute(recoverLikelyDroppedLeadingSlashPath(expandHome(rawDir)))
2606
+ ? recoverLikelyDroppedLeadingSlashPath(expandHome(rawDir))
2607
+ : resolve(baseDir, recoverLikelyDroppedLeadingSlashPath(expandHome(rawDir))))
2608
+ : baseDir;
2609
+ const currentReal = realpathSync(requested);
2610
+ const currentStat = statSync(currentReal);
2611
+ if (!currentStat.isDirectory()) throw new Error("File browser path does not refer to a directory.");
2612
+ if (!isPathInsideOrEqualDirectory(currentReal, rootReal)) {
2613
+ throw new Error("File browser path must stay within the current Studio resource directory.");
2614
+ }
2615
+ const parent = dirname(currentReal);
2616
+ const parentDir = parent !== currentReal && isPathInsideOrEqualDirectory(parent, rootReal) ? parent : null;
2617
+ const relativeDir = relative(rootReal, currentReal) || ".";
2618
+ return { rootDir: rootReal, currentDir: currentReal, relativeDir, parentDir };
2619
+ }
2620
+
2621
+ function listStudioFileBrowserDirectory(
2622
+ dirPath: string | undefined,
2623
+ sourcePath: string | undefined,
2624
+ resourceDir: string | undefined,
2625
+ fallbackCwd: string,
2626
+ ): { rootDir: string; currentDir: string; relativeDir: string; parentDir: string | null; entries: StudioFileBrowserEntry[]; omitted: number; omittedIgnored: number } {
2627
+ const context = resolveStudioFileBrowserDirectory(dirPath, sourcePath, resourceDir, fallbackCwd);
2628
+ const entries: StudioFileBrowserEntry[] = [];
2629
+ let omitted = 0;
2630
+ let omittedIgnored = 0;
2631
+ const dirents = readdirSync(context.currentDir, { withFileTypes: true });
2632
+ for (const dirent of dirents) {
2633
+ const name = dirent.name;
2634
+ if (STUDIO_FILE_BROWSER_IGNORED_DIRS.has(name)) {
2635
+ omittedIgnored += 1;
2636
+ continue;
2637
+ }
2638
+ const candidate = join(context.currentDir, name);
2639
+ try {
2640
+ const real = realpathSync(candidate);
2641
+ if (!isPathInsideOrEqualDirectory(real, context.rootDir)) {
2642
+ omitted += 1;
2643
+ continue;
2644
+ }
2645
+ const stat = statSync(real);
2646
+ if (!stat.isDirectory() && !stat.isFile()) {
2647
+ omitted += 1;
2648
+ continue;
2649
+ }
2650
+ const type = stat.isDirectory() ? "directory" : "file";
2651
+ const extension = type === "file" ? extname(real).toLowerCase() : "";
2652
+ entries.push({
2653
+ name,
2654
+ path: real,
2655
+ type,
2656
+ extension,
2657
+ kind: type === "directory" ? "directory" : getStudioLocalPreviewResourceKind(extension, real),
2658
+ size: stat.size,
2659
+ mtimeMs: stat.mtimeMs,
2660
+ hidden: name.startsWith("."),
2661
+ });
2662
+ } catch {
2663
+ omitted += 1;
2664
+ }
2665
+ }
2666
+ entries.sort((a, b) => {
2667
+ if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
2668
+ if (a.hidden !== b.hidden) return a.hidden ? 1 : -1;
2669
+ return a.name.localeCompare(b.name, undefined, { sensitivity: "base", numeric: true });
2670
+ });
2671
+ const limitedEntries = entries.slice(0, STUDIO_FILE_BROWSER_MAX_ENTRIES);
2672
+ omitted += Math.max(0, entries.length - limitedEntries.length);
2673
+ return { ...context, entries: limitedEntries, omitted, omittedIgnored };
2674
+ }
2675
+
2572
2676
  function resolveStudioHtmlPreviewResourcePath(
2573
2677
  resourcePath: string | undefined,
2574
2678
  sourcePath: string | undefined,
@@ -8958,7 +9062,7 @@ function resolveRequestedStudioDocumentFromUrl(
8958
9062
  label: requestedLabel || file.label,
8959
9063
  source: "file",
8960
9064
  path: file.resolvedPath,
8961
- resourceDir: requestedResourceDir || dirname(file.resolvedPath),
9065
+ resourceDir: requestedResourceDir || fallback?.resourceDir || studioCwd,
8962
9066
  };
8963
9067
  }
8964
9068
  }
@@ -9314,7 +9418,7 @@ ${cssVarsBlock}
9314
9418
  <button id="saveAsBtn" type="button" title="Save editor content to a new file path. Cmd/Ctrl+S falls back here when no direct save path is available.">Save editor as…</button>
9315
9419
  <button id="saveOverBtn" type="button" title="Overwrite current file with editor content. Shortcut: Cmd/Ctrl+S.">Save editor</button>
9316
9420
  <button id="refreshFromDiskBtn" type="button" title="Reload the current file-backed document from disk.">Refresh from disk</button>
9317
- <button id="clearWorkspaceBtn" type="button" title="Clear the current editor draft in this browser tab. Saved files and responses are not changed.">Clear editor</button>
9421
+ <button id="clearWorkspaceBtn" type="button" title="Clear editor text and reset this tab to a fresh blank draft. Saved files and responses are not changed.">Reset editor</button>
9318
9422
  <label class="file-label" title="Load a local file into editor text.">Load file content<input id="fileInput" type="file" accept=".md,.markdown,.mdx,.qmd,.js,.mjs,.cjs,.jsx,.ts,.mts,.cts,.tsx,.py,.pyw,.sh,.bash,.zsh,.json,.jsonc,.json5,.rs,.c,.h,.cpp,.cxx,.cc,.hpp,.hxx,.jl,.f90,.f95,.f03,.f,.for,.r,.R,.m,.tex,.latex,.diff,.patch,.java,.go,.rb,.swift,.html,.htm,.css,.xml,.yaml,.yml,.toml,.lua,.txt,.rst,.adoc" /></label>
9319
9423
  <button id="loadGitDiffBtn" type="button" title="Load the current git diff from the Studio context into the editor.">Load git diff</button>
9320
9424
  <button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
@@ -9508,6 +9612,7 @@ ${cssVarsBlock}
9508
9612
  <option value="preview" selected>Response (Preview)</option>
9509
9613
  <option value="editor-preview">Editor (Preview)</option>
9510
9614
  <option value="trace">Working</option>
9615
+ <option value="files">Files</option>
9511
9616
  <option value="repl">REPL</option>
9512
9617
  </select>
9513
9618
  </div>
@@ -12677,6 +12782,34 @@ export default function (pi: ExtensionAPI) {
12677
12782
  return;
12678
12783
  }
12679
12784
 
12785
+ if (requestUrl.pathname === "/file-browser") {
12786
+ const token = requestUrl.searchParams.get("token") ?? "";
12787
+ if (token !== serverState.token) {
12788
+ respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
12789
+ return;
12790
+ }
12791
+
12792
+ const method = (req.method ?? "GET").toUpperCase();
12793
+ if (method !== "GET" && method !== "HEAD") {
12794
+ res.setHeader("Allow", "GET, HEAD");
12795
+ respondJson(res, 405, { ok: false, error: "Method not allowed. Use GET." });
12796
+ return;
12797
+ }
12798
+
12799
+ try {
12800
+ const listing = listStudioFileBrowserDirectory(
12801
+ requestUrl.searchParams.get("dir") ?? undefined,
12802
+ requestUrl.searchParams.get("sourcePath") ?? undefined,
12803
+ requestUrl.searchParams.get("resourceDir") ?? undefined,
12804
+ studioCwd,
12805
+ );
12806
+ respondJson(res, 200, { ok: true, ...listing, entries: method === "HEAD" ? [] : listing.entries });
12807
+ } catch (error) {
12808
+ respondJson(res, 404, { ok: false, error: `File browser unavailable: ${error instanceof Error ? error.message : String(error)}` });
12809
+ }
12810
+ return;
12811
+ }
12812
+
12680
12813
  if (requestUrl.pathname === "/local-preview-link") {
12681
12814
  const token = requestUrl.searchParams.get("token") ?? "";
12682
12815
  if (token !== serverState.token) {
@@ -13377,7 +13510,7 @@ export default function (pi: ExtensionAPI) {
13377
13510
  label: file.label,
13378
13511
  source: "file",
13379
13512
  path: file.resolvedPath,
13380
- resourceDir: dirname(file.resolvedPath),
13513
+ resourceDir: ctx.cwd,
13381
13514
  };
13382
13515
  };
13383
13516
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.9.14",
3
+ "version": "0.9.15",
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",