pi-studio 0.9.13 → 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 +27 -1
- package/README.md +2 -1
- package/client/studio-client.js +381 -19
- package/client/studio.css +163 -4
- package/index.ts +152 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,10 +4,36 @@ 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
|
+
|
|
21
|
+
## [0.9.14] — 2026-05-22
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- Added active-pane text-size shortcuts: Alt/Option+= to increase, Alt/Option+- to decrease, and Alt/Option+0 to reset editor or right-pane text size when not editing text.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- Blank and last-response Studio launches now default the working directory/resource context to the current Pi directory.
|
|
28
|
+
- Made the working-directory label use normal muted text styling instead of standing out like a primary control.
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- Fixed the keyboard-shortcuts overlay layout so shortcut groups are no longer clipped by the global pane section styles.
|
|
32
|
+
|
|
7
33
|
## [0.9.13] — 2026-05-22
|
|
8
34
|
|
|
9
35
|
### Added
|
|
10
|
-
- Added browser-refresh restoration for the Studio editor workspace, plus an explicit **
|
|
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.
|
|
11
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.
|
|
12
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.
|
|
13
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 **
|
|
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:
|
package/client/studio-client.js
CHANGED
|
@@ -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) {
|
|
@@ -2174,6 +2187,49 @@
|
|
|
2174
2187
|
scheduleResponsePaneRepaintNudge();
|
|
2175
2188
|
}
|
|
2176
2189
|
|
|
2190
|
+
function getActivePaneTextSizeConfig() {
|
|
2191
|
+
if (activePane === "right") {
|
|
2192
|
+
return {
|
|
2193
|
+
label: "Right pane text size",
|
|
2194
|
+
value: responseFontSize,
|
|
2195
|
+
defaultValue: DEFAULT_RESPONSE_FONT_SIZE,
|
|
2196
|
+
options: RESPONSE_FONT_SIZE_OPTIONS,
|
|
2197
|
+
setValue: setResponseFontSize,
|
|
2198
|
+
};
|
|
2199
|
+
}
|
|
2200
|
+
return {
|
|
2201
|
+
label: "Editor text size",
|
|
2202
|
+
value: editorFontSize,
|
|
2203
|
+
defaultValue: DEFAULT_EDITOR_FONT_SIZE,
|
|
2204
|
+
options: EDITOR_FONT_SIZE_OPTIONS,
|
|
2205
|
+
setValue: setEditorFontSize,
|
|
2206
|
+
};
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
function getNextStudioFontSizeOption(currentValue, options, defaultValue, direction) {
|
|
2210
|
+
const normalized = normalizeStudioFontSize(currentValue, options, defaultValue);
|
|
2211
|
+
const currentIndex = Math.max(0, options.findIndex((option) => Math.abs(option - normalized) < 0.001));
|
|
2212
|
+
const nextIndex = Math.max(0, Math.min(options.length - 1, currentIndex + direction));
|
|
2213
|
+
return options[nextIndex];
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
function adjustActivePaneTextSize(direction) {
|
|
2217
|
+
const config = getActivePaneTextSizeConfig();
|
|
2218
|
+
const nextSize = getNextStudioFontSizeOption(config.value, config.options, config.defaultValue, direction);
|
|
2219
|
+
if (Math.abs(nextSize - config.value) < 0.001) {
|
|
2220
|
+
setStatus(config.label + " already at " + formatStudioFontSizeLabel(nextSize) + ".", "warning");
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
config.setValue(nextSize);
|
|
2224
|
+
setStatus(config.label + ": " + formatStudioFontSizeLabel(nextSize) + ".");
|
|
2225
|
+
}
|
|
2226
|
+
|
|
2227
|
+
function resetActivePaneTextSize() {
|
|
2228
|
+
const config = getActivePaneTextSizeConfig();
|
|
2229
|
+
config.setValue(config.defaultValue);
|
|
2230
|
+
setStatus(config.label + " reset to " + formatStudioFontSizeLabel(config.defaultValue) + ".");
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2177
2233
|
function getStudioUiRefreshAnnotationHeaderEnabled() {
|
|
2178
2234
|
try {
|
|
2179
2235
|
return Boolean(stripAnnotationHeader(sourceTextEl.value).hadHeader);
|
|
@@ -3648,6 +3704,24 @@
|
|
|
3648
3704
|
}
|
|
3649
3705
|
}
|
|
3650
3706
|
|
|
3707
|
+
if (!isTextEntryShortcutTarget(event.target) && !event.metaKey && !event.ctrlKey && event.altKey) {
|
|
3708
|
+
if (code === "Equal" || code === "NumpadAdd" || key === "=" || key === "+") {
|
|
3709
|
+
event.preventDefault();
|
|
3710
|
+
adjustActivePaneTextSize(1);
|
|
3711
|
+
return;
|
|
3712
|
+
}
|
|
3713
|
+
if (code === "Minus" || code === "NumpadSubtract" || key === "-" || key === "_") {
|
|
3714
|
+
event.preventDefault();
|
|
3715
|
+
adjustActivePaneTextSize(-1);
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
3718
|
+
if (code === "Digit0" || code === "Numpad0" || key === "0") {
|
|
3719
|
+
event.preventDefault();
|
|
3720
|
+
resetActivePaneTextSize();
|
|
3721
|
+
return;
|
|
3722
|
+
}
|
|
3723
|
+
}
|
|
3724
|
+
|
|
3651
3725
|
const isPaneSwitchShortcut = key === "F6" && !event.metaKey && !event.ctrlKey && !event.altKey;
|
|
3652
3726
|
if (isPaneSwitchShortcut) {
|
|
3653
3727
|
event.preventDefault();
|
|
@@ -4024,6 +4098,12 @@
|
|
|
4024
4098
|
}
|
|
4025
4099
|
if (referenceMetaEl instanceof HTMLElement) referenceMetaEl.hidden = false;
|
|
4026
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
|
+
|
|
4027
4107
|
if (rightView === "trace") {
|
|
4028
4108
|
const state = traceState || createEmptyTraceState();
|
|
4029
4109
|
const context = traceDisplayContext || {};
|
|
@@ -6584,6 +6664,7 @@
|
|
|
6584
6664
|
critiqueViewEl.addEventListener("scroll", handleTracePaneScroll);
|
|
6585
6665
|
critiqueViewEl.addEventListener("click", handleTracePaneClick);
|
|
6586
6666
|
critiqueViewEl.addEventListener("click", handleReplPaneClick);
|
|
6667
|
+
critiqueViewEl.addEventListener("click", handleFilesPaneClick);
|
|
6587
6668
|
critiqueViewEl.addEventListener("change", handleReplPaneChange);
|
|
6588
6669
|
}
|
|
6589
6670
|
|
|
@@ -8046,6 +8127,241 @@
|
|
|
8046
8127
|
scheduleResponsePaneRepaintNudge();
|
|
8047
8128
|
}
|
|
8048
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
|
+
|
|
8049
8365
|
function renderActiveResult() {
|
|
8050
8366
|
if (rightView === "trace") {
|
|
8051
8367
|
renderTraceView();
|
|
@@ -8057,6 +8373,11 @@
|
|
|
8057
8373
|
return;
|
|
8058
8374
|
}
|
|
8059
8375
|
|
|
8376
|
+
if (rightView === "files") {
|
|
8377
|
+
renderFilesView();
|
|
8378
|
+
return;
|
|
8379
|
+
}
|
|
8380
|
+
|
|
8060
8381
|
if (rightView === "editor-preview") {
|
|
8061
8382
|
const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
|
|
8062
8383
|
if (!editorText.trim()) {
|
|
@@ -8131,7 +8452,7 @@
|
|
|
8131
8452
|
: normalizeForCompare(sourceTextEl.value);
|
|
8132
8453
|
const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
|
|
8133
8454
|
const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
|
|
8134
|
-
const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl";
|
|
8455
|
+
const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl" || rightView === "files";
|
|
8135
8456
|
|
|
8136
8457
|
if (responseWrapEl) {
|
|
8137
8458
|
responseWrapEl.hidden = showingAuxiliaryRightPane;
|
|
@@ -8172,6 +8493,8 @@
|
|
|
8172
8493
|
: (exportingReplJournal ? "Export record" : "Export right preview");
|
|
8173
8494
|
if (rightView === "trace") {
|
|
8174
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.";
|
|
8175
8498
|
} else if (exportingReplJournal && !replJournalExportEntries.length) {
|
|
8176
8499
|
exportPdfBtn.title = "No Studio REPL record entries to export for this session yet.";
|
|
8177
8500
|
} else if (rightView === "markdown") {
|
|
@@ -8404,9 +8727,24 @@
|
|
|
8404
8727
|
return "source:" + normalized.source + ":" + normalized.label;
|
|
8405
8728
|
}
|
|
8406
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
|
+
|
|
8407
8744
|
function readPersistedWorkspaceState() {
|
|
8408
8745
|
try {
|
|
8409
|
-
const
|
|
8746
|
+
const storage = getWorkspacePersistenceStorage();
|
|
8747
|
+
const raw = storage ? storage.getItem(STUDIO_WORKSPACE_STORAGE_KEY) : null;
|
|
8410
8748
|
if (!raw) return null;
|
|
8411
8749
|
const parsed = JSON.parse(raw);
|
|
8412
8750
|
if (!parsed || typeof parsed !== "object" || parsed.version !== 1) return null;
|
|
@@ -8434,7 +8772,7 @@
|
|
|
8434
8772
|
sourceState: normalizeWorkspaceSourceState(sourceState),
|
|
8435
8773
|
resourceDir: getCurrentResourceDirValue(),
|
|
8436
8774
|
editorView,
|
|
8437
|
-
rightView,
|
|
8775
|
+
rightView: isEditorOnlyMode ? "editor-preview" : rightView,
|
|
8438
8776
|
editorLanguage,
|
|
8439
8777
|
followLatest,
|
|
8440
8778
|
responseHistoryIndex,
|
|
@@ -8448,13 +8786,15 @@
|
|
|
8448
8786
|
function persistWorkspaceStateNow() {
|
|
8449
8787
|
if (!workspacePersistenceReady) return;
|
|
8450
8788
|
try {
|
|
8451
|
-
|
|
8789
|
+
const storage = getWorkspacePersistenceStorage();
|
|
8790
|
+
if (!storage) return;
|
|
8791
|
+
clearLegacyWorkspacePersistenceStorage();
|
|
8452
8792
|
const payload = buildWorkspacePersistencePayload();
|
|
8453
8793
|
if (payload.text.length > STUDIO_WORKSPACE_MAX_TEXT_CHARS) {
|
|
8454
|
-
|
|
8794
|
+
storage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
|
|
8455
8795
|
return;
|
|
8456
8796
|
}
|
|
8457
|
-
|
|
8797
|
+
storage.setItem(STUDIO_WORKSPACE_STORAGE_KEY, JSON.stringify(payload));
|
|
8458
8798
|
} catch {
|
|
8459
8799
|
// Ignore browser storage failures and quota limits.
|
|
8460
8800
|
}
|
|
@@ -8483,8 +8823,10 @@
|
|
|
8483
8823
|
workspacePersistTimer = null;
|
|
8484
8824
|
}
|
|
8485
8825
|
try {
|
|
8486
|
-
|
|
8826
|
+
const storage = getWorkspacePersistenceStorage();
|
|
8827
|
+
if (storage) storage.removeItem(STUDIO_WORKSPACE_STORAGE_KEY);
|
|
8487
8828
|
} catch {}
|
|
8829
|
+
clearLegacyWorkspacePersistenceStorage();
|
|
8488
8830
|
}
|
|
8489
8831
|
|
|
8490
8832
|
function applyPersistedWorkspaceState(state) {
|
|
@@ -8502,11 +8844,15 @@
|
|
|
8502
8844
|
setEditorLanguage(state.editorLanguage.trim());
|
|
8503
8845
|
}
|
|
8504
8846
|
editorView = state.editorView === "preview" ? "preview" : "markdown";
|
|
8505
|
-
rightView =
|
|
8506
|
-
? "preview"
|
|
8507
|
-
: (state.rightView === "
|
|
8508
|
-
? "
|
|
8509
|
-
: (state.rightView === "
|
|
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")))));
|
|
8510
8856
|
if (typeof state.followLatest === "boolean") {
|
|
8511
8857
|
followLatest = state.followLatest;
|
|
8512
8858
|
}
|
|
@@ -8531,7 +8877,7 @@
|
|
|
8531
8877
|
setStatus("Studio is busy.", "warning");
|
|
8532
8878
|
return;
|
|
8533
8879
|
}
|
|
8534
|
-
const confirmed = window.confirm("
|
|
8880
|
+
const confirmed = window.confirm("Reset the editor to a fresh blank draft in this browser tab? Saved files and responses are not changed.");
|
|
8535
8881
|
if (!confirmed) return;
|
|
8536
8882
|
const preservedResponseState = {
|
|
8537
8883
|
responseHistory: Array.isArray(responseHistory) ? responseHistory.slice() : [],
|
|
@@ -8573,7 +8919,7 @@
|
|
|
8573
8919
|
if (followSelect) followSelect.value = followLatest ? "on" : "off";
|
|
8574
8920
|
refreshResponseUi();
|
|
8575
8921
|
persistWorkspaceStateNow();
|
|
8576
|
-
setStatus("Editor
|
|
8922
|
+
setStatus("Editor reset to a fresh blank draft. Saved files and responses were not changed.", "success");
|
|
8577
8923
|
}
|
|
8578
8924
|
|
|
8579
8925
|
function setEditorText(nextText, options) {
|
|
@@ -8772,7 +9118,9 @@
|
|
|
8772
9118
|
? "editor-preview"
|
|
8773
9119
|
: (nextView === "repl"
|
|
8774
9120
|
? "repl"
|
|
8775
|
-
: (
|
|
9121
|
+
: (nextView === "files"
|
|
9122
|
+
? "files"
|
|
9123
|
+
: ((nextView === "trace" || nextView === "thinking") ? "trace" : "markdown"))));
|
|
8776
9124
|
rightViewSelect.value = rightView;
|
|
8777
9125
|
if (rightView === "trace" && previousView !== "trace") {
|
|
8778
9126
|
traceAutoScroll = true;
|
|
@@ -9112,6 +9460,11 @@
|
|
|
9112
9460
|
".diff", ".patch",
|
|
9113
9461
|
]);
|
|
9114
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
|
+
]);
|
|
9115
9468
|
let previewLinkMenuEl = null;
|
|
9116
9469
|
let activePreviewLinkContext = null;
|
|
9117
9470
|
|
|
@@ -9159,10 +9512,17 @@
|
|
|
9159
9512
|
return match ? ("." + match[1].toLowerCase()) : "";
|
|
9160
9513
|
}
|
|
9161
9514
|
|
|
9515
|
+
function getPreviewLocalLinkFilename(href) {
|
|
9516
|
+
const path = stripPreviewLocalLinkUrlSuffix(href).replace(/\\/g, "/");
|
|
9517
|
+
const parts = path.split("/");
|
|
9518
|
+
return (parts.pop() || "").toLowerCase();
|
|
9519
|
+
}
|
|
9520
|
+
|
|
9162
9521
|
function getPreviewLocalLinkKind(href) {
|
|
9163
9522
|
const ext = getPreviewLocalLinkExtension(href);
|
|
9523
|
+
const name = getPreviewLocalLinkFilename(href);
|
|
9164
9524
|
if (ext === ".pdf") return "pdf";
|
|
9165
|
-
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";
|
|
9166
9526
|
if (PREVIEW_LOCAL_IMAGE_LINK_EXTENSIONS.has(ext)) return "image";
|
|
9167
9527
|
return "other";
|
|
9168
9528
|
}
|
|
@@ -9179,9 +9539,11 @@
|
|
|
9179
9539
|
function getEffectivePreviewLinkContext(contextOverride) {
|
|
9180
9540
|
const fallback = getHtmlPreviewResourceContextOptions();
|
|
9181
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"));
|
|
9182
9544
|
return {
|
|
9183
|
-
sourcePath:
|
|
9184
|
-
resourceDir:
|
|
9545
|
+
sourcePath: hasSourcePath ? String(context.sourcePath || "") : (fallback.sourcePath || ""),
|
|
9546
|
+
resourceDir: hasResourceDir ? String(context.resourceDir || "") : (fallback.resourceDir || ""),
|
|
9185
9547
|
};
|
|
9186
9548
|
}
|
|
9187
9549
|
|
|
@@ -17830,7 +18192,7 @@
|
|
|
17830
18192
|
renderSourcePreview();
|
|
17831
18193
|
workspacePersistenceReady = true;
|
|
17832
18194
|
if (workspaceRestoredFromBrowser) {
|
|
17833
|
-
setStatus("Restored editor workspace from this browser tab. Use
|
|
18195
|
+
setStatus("Restored editor workspace from this browser tab. Use Reset editor to discard it.", "success");
|
|
17834
18196
|
}
|
|
17835
18197
|
connect();
|
|
17836
18198
|
} catch (error) {
|
package/client/studio.css
CHANGED
|
@@ -641,6 +641,7 @@
|
|
|
641
641
|
overflow: hidden;
|
|
642
642
|
text-overflow: ellipsis;
|
|
643
643
|
white-space: nowrap;
|
|
644
|
+
font-weight: 400;
|
|
644
645
|
}
|
|
645
646
|
.resource-dir-label:hover {
|
|
646
647
|
color: var(--fg);
|
|
@@ -2665,6 +2666,148 @@
|
|
|
2665
2666
|
flex: 0 0 auto;
|
|
2666
2667
|
}
|
|
2667
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
|
+
|
|
2668
2811
|
.trace-panel {
|
|
2669
2812
|
display: flex;
|
|
2670
2813
|
flex-direction: column;
|
|
@@ -3583,17 +3726,29 @@
|
|
|
3583
3726
|
|
|
3584
3727
|
.shortcuts-body {
|
|
3585
3728
|
display: grid;
|
|
3586
|
-
gap:
|
|
3587
|
-
padding:
|
|
3729
|
+
gap: 16px;
|
|
3730
|
+
padding: 16px 18px 18px;
|
|
3588
3731
|
overflow-y: auto;
|
|
3589
3732
|
overflow-x: hidden;
|
|
3590
3733
|
min-height: 0;
|
|
3591
3734
|
background: var(--panel);
|
|
3592
3735
|
}
|
|
3593
3736
|
|
|
3737
|
+
.shortcuts-group {
|
|
3738
|
+
display: block;
|
|
3739
|
+
min-height: auto;
|
|
3740
|
+
border: 0;
|
|
3741
|
+
border-radius: 0;
|
|
3742
|
+
background: transparent;
|
|
3743
|
+
box-shadow: none;
|
|
3744
|
+
overflow: visible;
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3594
3747
|
.shortcuts-group h3 {
|
|
3595
|
-
margin: 0 0
|
|
3748
|
+
margin: 0 0 8px;
|
|
3749
|
+
padding-top: 3px;
|
|
3596
3750
|
font-size: 12px;
|
|
3751
|
+
line-height: 1.5;
|
|
3597
3752
|
font-weight: 700;
|
|
3598
3753
|
color: var(--studio-info-text, var(--muted));
|
|
3599
3754
|
text-transform: uppercase;
|
|
@@ -4259,7 +4414,6 @@
|
|
|
4259
4414
|
}
|
|
4260
4415
|
|
|
4261
4416
|
body.studio-ui-refresh #resourceDirBtn,
|
|
4262
|
-
body.studio-ui-refresh #resourceDirLabel,
|
|
4263
4417
|
body.studio-ui-refresh #reviewNotesBtn,
|
|
4264
4418
|
body.studio-ui-refresh #outlineBtn,
|
|
4265
4419
|
body.studio-ui-refresh #scratchpadBtn,
|
|
@@ -4267,6 +4421,11 @@
|
|
|
4267
4421
|
color: var(--text);
|
|
4268
4422
|
}
|
|
4269
4423
|
|
|
4424
|
+
body.studio-ui-refresh #resourceDirLabel {
|
|
4425
|
+
color: var(--studio-info-text, var(--muted));
|
|
4426
|
+
font-weight: 400;
|
|
4427
|
+
}
|
|
4428
|
+
|
|
4270
4429
|
body.studio-ui-refresh #resourceDirInputWrap.visible {
|
|
4271
4430
|
display: inline-flex;
|
|
4272
4431
|
}
|
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 ||
|
|
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
|
|
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>
|
|
@@ -9600,6 +9705,14 @@ ${cssVarsBlock}
|
|
|
9600
9705
|
<div><dt>?</dt><dd>Show keyboard shortcuts when not editing text</dd></div>
|
|
9601
9706
|
</dl>
|
|
9602
9707
|
</section>
|
|
9708
|
+
<section class="shortcuts-group">
|
|
9709
|
+
<h3>View</h3>
|
|
9710
|
+
<dl>
|
|
9711
|
+
<div><dt>Alt/Option+=</dt><dd>Increase the active pane's text size when not editing text</dd></div>
|
|
9712
|
+
<div><dt>Alt/Option+-</dt><dd>Decrease the active pane's text size when not editing text</dd></div>
|
|
9713
|
+
<div><dt>Alt/Option+0</dt><dd>Reset the active pane's text size when not editing text</dd></div>
|
|
9714
|
+
</dl>
|
|
9715
|
+
</section>
|
|
9603
9716
|
<section class="shortcuts-group">
|
|
9604
9717
|
<h3>Editor</h3>
|
|
9605
9718
|
<dl>
|
|
@@ -12669,6 +12782,34 @@ export default function (pi: ExtensionAPI) {
|
|
|
12669
12782
|
return;
|
|
12670
12783
|
}
|
|
12671
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
|
+
|
|
12672
12813
|
if (requestUrl.pathname === "/local-preview-link") {
|
|
12673
12814
|
const token = requestUrl.searchParams.get("token") ?? "";
|
|
12674
12815
|
if (token !== serverState.token) {
|
|
@@ -13298,6 +13439,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
13298
13439
|
label: "last model response",
|
|
13299
13440
|
source: "last-response",
|
|
13300
13441
|
draftId: createStudioDraftId(),
|
|
13442
|
+
resourceDir: ctx.cwd,
|
|
13301
13443
|
};
|
|
13302
13444
|
}
|
|
13303
13445
|
return {
|
|
@@ -13305,6 +13447,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
13305
13447
|
label: "blank",
|
|
13306
13448
|
source: "blank",
|
|
13307
13449
|
draftId: createStudioDraftId(),
|
|
13450
|
+
resourceDir: ctx.cwd,
|
|
13308
13451
|
};
|
|
13309
13452
|
}
|
|
13310
13453
|
|
|
@@ -13314,6 +13457,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
13314
13457
|
label: "blank",
|
|
13315
13458
|
source: "blank",
|
|
13316
13459
|
draftId: createStudioDraftId(),
|
|
13460
|
+
resourceDir: ctx.cwd,
|
|
13317
13461
|
};
|
|
13318
13462
|
}
|
|
13319
13463
|
|
|
@@ -13325,6 +13469,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
13325
13469
|
label: "blank",
|
|
13326
13470
|
source: "blank",
|
|
13327
13471
|
draftId: createStudioDraftId(),
|
|
13472
|
+
resourceDir: ctx.cwd,
|
|
13328
13473
|
};
|
|
13329
13474
|
}
|
|
13330
13475
|
return {
|
|
@@ -13332,6 +13477,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
13332
13477
|
label: "last model response",
|
|
13333
13478
|
source: "last-response",
|
|
13334
13479
|
draftId: createStudioDraftId(),
|
|
13480
|
+
resourceDir: ctx.cwd,
|
|
13335
13481
|
};
|
|
13336
13482
|
}
|
|
13337
13483
|
|
|
@@ -13364,7 +13510,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
13364
13510
|
label: file.label,
|
|
13365
13511
|
source: "file",
|
|
13366
13512
|
path: file.resolvedPath,
|
|
13367
|
-
resourceDir:
|
|
13513
|
+
resourceDir: ctx.cwd,
|
|
13368
13514
|
};
|
|
13369
13515
|
};
|
|
13370
13516
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
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",
|