pi-studio 0.9.34 → 0.9.35
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 +5 -0
- package/README.md +1 -1
- package/client/studio-client.js +67 -0
- package/client/studio.css +11 -1
- package/index.ts +30 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,11 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.35] — 2026-07-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Added Files-view sort options for name, newest/oldest modified time, and largest/smallest file size.
|
|
11
|
+
|
|
7
12
|
## [0.9.34] — 2026-07-02
|
|
8
13
|
|
|
9
14
|
### Added
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ _The video shows an earlier version of the Studio interface. The basic workflow
|
|
|
34
34
|
- Runs editor text directly, asks for structured critique (auto/writing/code focus), offers a manual **Suggest completion** action for short cursor-aware continuations (`Option/Alt+Tab` where available or `Cmd/Ctrl+Shift+Space` from the editor, `Tab` to insert a visible suggestion) with an optional editor-plus-latest-response context mode, or opens **Quiz me** for a Studio-native active-recall loop over the current editor text, selection, current file, folder, or repo, with optional focus guidance for shaping question selection
|
|
35
35
|
- 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, and `Cmd/Ctrl+Alt+1–7` switches directly between right-pane views while `Cmd/Ctrl+Alt+P` / `Cmd/Ctrl+Alt+E` / `Cmd/Ctrl+Alt+W` keep quick mnemonic shortcuts for Response Preview, Editor Preview, and Working
|
|
36
36
|
- Includes a right-pane **Changes** view for browsing the current git diff by file, previewing per-file diffs, opening changed files, loading the full diff into the editor, and copying the diff
|
|
37
|
-
- Includes a right-pane **Files** view for browsing the current Pi session/resource directory, opening folders, opening the Files root in Finder/the system file manager, loading text/code/CSV/TSV documents into the editor, previewing PDFs/images, opening PDF/image previews in a new Studio tab, converting DOCX/ODT documents to editable Markdown when Pandoc is available after confirmation, copying paths, setting the current folder as the Studio working directory, and revealing files in the file manager
|
|
37
|
+
- Includes a right-pane **Files** view for browsing the current Pi session/resource directory, sorting by name/modified time/size, opening folders, opening the Files root in Finder/the system file manager, loading text/code/CSV/TSV documents into the editor, previewing PDFs/images, opening PDF/image previews in a new Studio tab, converting DOCX/ODT documents to editable Markdown when Pandoc is available after confirmation, copying paths, setting the current folder as the Studio working directory, and revealing files in the file manager
|
|
38
38
|
- 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
|
|
39
39
|
- Includes a local persistent scratchpad for quick notes you want to keep out of the main editor until you're ready to copy or insert them, with a **Recent…** picker for recovering scratchpads saved under earlier file/draft identities
|
|
40
40
|
- Includes a docked **Outline** rail for navigating document structure in the current editor text, with clickable entries that jump in the raw editor and reveal matching preview locations when available
|
package/client/studio-client.js
CHANGED
|
@@ -2104,6 +2104,35 @@
|
|
|
2104
2104
|
const DEFAULT_RESPONSE_FONT_SIZE = studioUiRefreshEnabled ? 13.5 : 15;
|
|
2105
2105
|
let editorFontSize = DEFAULT_EDITOR_FONT_SIZE;
|
|
2106
2106
|
let responseFontSize = DEFAULT_RESPONSE_FONT_SIZE;
|
|
2107
|
+
function normalizeFileBrowserSortMode(sort) {
|
|
2108
|
+
const value = String(sort || "").trim().toLowerCase();
|
|
2109
|
+
if (value === "mtime-desc" || value === "modified-desc" || value === "newest") return "mtime-desc";
|
|
2110
|
+
if (value === "mtime-asc" || value === "modified-asc" || value === "oldest") return "mtime-asc";
|
|
2111
|
+
if (value === "size-desc" || value === "largest") return "size-desc";
|
|
2112
|
+
if (value === "size-asc" || value === "smallest") return "size-asc";
|
|
2113
|
+
return "name";
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
function readFileBrowserSortMode() {
|
|
2117
|
+
try {
|
|
2118
|
+
const stored = window.localStorage ? window.localStorage.getItem("piStudio.fileBrowserSort") : "";
|
|
2119
|
+
return normalizeFileBrowserSortMode(stored);
|
|
2120
|
+
} catch {
|
|
2121
|
+
return "name";
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
|
|
2125
|
+
function writeFileBrowserSortMode(sort) {
|
|
2126
|
+
const normalized = normalizeFileBrowserSortMode(sort);
|
|
2127
|
+
try {
|
|
2128
|
+
if (window.localStorage) window.localStorage.setItem("piStudio.fileBrowserSort", normalized);
|
|
2129
|
+
} catch {
|
|
2130
|
+
// Ignore storage failures.
|
|
2131
|
+
}
|
|
2132
|
+
return normalized;
|
|
2133
|
+
}
|
|
2134
|
+
|
|
2135
|
+
let fileBrowserSortMode = readFileBrowserSortMode();
|
|
2107
2136
|
let fileBrowserState = {
|
|
2108
2137
|
rootDir: "",
|
|
2109
2138
|
currentDir: "",
|
|
@@ -2112,6 +2141,7 @@
|
|
|
2112
2141
|
entries: [],
|
|
2113
2142
|
omitted: 0,
|
|
2114
2143
|
omittedIgnored: 0,
|
|
2144
|
+
sort: fileBrowserSortMode,
|
|
2115
2145
|
loading: false,
|
|
2116
2146
|
error: "",
|
|
2117
2147
|
loaded: false,
|
|
@@ -8145,6 +8175,9 @@
|
|
|
8145
8175
|
critiqueViewEl.addEventListener("click", handleFilesPaneClick);
|
|
8146
8176
|
critiqueViewEl.addEventListener("click", handleGitChangesPaneClick);
|
|
8147
8177
|
critiqueViewEl.addEventListener("change", handleReplPaneChange);
|
|
8178
|
+
critiqueViewEl.addEventListener("change", (event) => {
|
|
8179
|
+
void handleFilesPaneChange(event);
|
|
8180
|
+
});
|
|
8148
8181
|
}
|
|
8149
8182
|
|
|
8150
8183
|
function replaceResponsePaneWithClone() {
|
|
@@ -9965,6 +9998,24 @@
|
|
|
9965
9998
|
return entry.extension ? entry.extension.replace(/^\./, "") : "file";
|
|
9966
9999
|
}
|
|
9967
10000
|
|
|
10001
|
+
function getFileBrowserSortOptions() {
|
|
10002
|
+
return [
|
|
10003
|
+
{ value: "name", label: "Sort: Name" },
|
|
10004
|
+
{ value: "mtime-desc", label: "Sort: Newest" },
|
|
10005
|
+
{ value: "mtime-asc", label: "Sort: Oldest" },
|
|
10006
|
+
{ value: "size-desc", label: "Sort: Largest" },
|
|
10007
|
+
{ value: "size-asc", label: "Sort: Smallest" },
|
|
10008
|
+
];
|
|
10009
|
+
}
|
|
10010
|
+
|
|
10011
|
+
function buildFileBrowserSortSelectHtml() {
|
|
10012
|
+
const currentSort = normalizeFileBrowserSortMode(fileBrowserSortMode || (fileBrowserState && fileBrowserState.sort));
|
|
10013
|
+
const options = getFileBrowserSortOptions().map((option) => {
|
|
10014
|
+
return "<option value='" + escapeHtml(option.value) + "'" + (option.value === currentSort ? " selected" : "") + ">" + escapeHtml(option.label) + "</option>";
|
|
10015
|
+
}).join("");
|
|
10016
|
+
return "<select class='files-sort-select' data-files-sort aria-label='Files sort order' title='Sort file browser entries. Folders remain grouped first.'>" + options + "</select>";
|
|
10017
|
+
}
|
|
10018
|
+
|
|
9968
10019
|
function buildFileBrowserPanelHtml() {
|
|
9969
10020
|
const state = fileBrowserState || {};
|
|
9970
10021
|
const entries = Array.isArray(state.entries) ? state.entries : [];
|
|
@@ -10019,6 +10070,7 @@
|
|
|
10019
10070
|
+ "<div class='files-toolbar'>"
|
|
10020
10071
|
+ "<div class='files-path-group'><span class='files-label'>Files</span><span class='files-path' title='" + escapeHtml(currentDir) + "'>" + escapeHtml(relativeDir || ".") + "</span></div>"
|
|
10021
10072
|
+ "<div class='files-toolbar-actions'>"
|
|
10073
|
+
+ buildFileBrowserSortSelectHtml()
|
|
10022
10074
|
+ "<button type='button' data-files-action='parent'" + parentDisabled + ">Parent</button>"
|
|
10023
10075
|
+ "<button type='button' data-files-action='refresh'>Refresh</button>"
|
|
10024
10076
|
+ (currentDir ? "<button type='button' data-files-action='copy-current' data-files-path='" + escapeHtml(currentDir) + "'>Copy path</button>" : "")
|
|
@@ -10045,6 +10097,7 @@
|
|
|
10045
10097
|
entries: [],
|
|
10046
10098
|
omitted: 0,
|
|
10047
10099
|
omittedIgnored: 0,
|
|
10100
|
+
sort: fileBrowserSortMode,
|
|
10048
10101
|
loading: false,
|
|
10049
10102
|
error: "",
|
|
10050
10103
|
loaded: false,
|
|
@@ -10079,8 +10132,10 @@
|
|
|
10079
10132
|
if (dir) query.dir = String(dir);
|
|
10080
10133
|
if (context.sourcePath) query.sourcePath = context.sourcePath;
|
|
10081
10134
|
if (context.resourceDir) query.resourceDir = context.resourceDir;
|
|
10135
|
+
query.sort = normalizeFileBrowserSortMode(fileBrowserSortMode);
|
|
10082
10136
|
const payload = await fetchStudioJson("/file-browser", { query });
|
|
10083
10137
|
if (nonce !== fileBrowserLoadNonce) return;
|
|
10138
|
+
fileBrowserSortMode = normalizeFileBrowserSortMode(payload.sort || fileBrowserSortMode);
|
|
10084
10139
|
fileBrowserState = {
|
|
10085
10140
|
rootDir: typeof payload.rootDir === "string" ? payload.rootDir : "",
|
|
10086
10141
|
currentDir: typeof payload.currentDir === "string" ? payload.currentDir : "",
|
|
@@ -10089,6 +10144,7 @@
|
|
|
10089
10144
|
entries: Array.isArray(payload.entries) ? payload.entries : [],
|
|
10090
10145
|
omitted: Number(payload.omitted) || 0,
|
|
10091
10146
|
omittedIgnored: Number(payload.omittedIgnored) || 0,
|
|
10147
|
+
sort: normalizeFileBrowserSortMode(payload.sort || fileBrowserSortMode),
|
|
10092
10148
|
loading: false,
|
|
10093
10149
|
error: "",
|
|
10094
10150
|
loaded: true,
|
|
@@ -10191,6 +10247,17 @@
|
|
|
10191
10247
|
setStatus(payload && payload.message ? payload.message : "Opened folder in file manager.", "success");
|
|
10192
10248
|
}
|
|
10193
10249
|
|
|
10250
|
+
async function handleFilesPaneChange(event) {
|
|
10251
|
+
if (rightView !== "files") return;
|
|
10252
|
+
const target = event.target;
|
|
10253
|
+
const sortSelect = target instanceof Element ? target.closest("[data-files-sort]") : null;
|
|
10254
|
+
if (!sortSelect || !("value" in sortSelect)) return;
|
|
10255
|
+
const nextSort = writeFileBrowserSortMode(sortSelect.value);
|
|
10256
|
+
if (nextSort === fileBrowserSortMode && fileBrowserState.loaded && !fileBrowserState.loading) return;
|
|
10257
|
+
fileBrowserSortMode = nextSort;
|
|
10258
|
+
await loadFileBrowserDirectory(fileBrowserState.currentDir || "", { user: true });
|
|
10259
|
+
}
|
|
10260
|
+
|
|
10194
10261
|
async function handleFilesPaneClick(event) {
|
|
10195
10262
|
if (rightView !== "files") return;
|
|
10196
10263
|
const target = event.target;
|
package/client/studio.css
CHANGED
|
@@ -3290,11 +3290,21 @@
|
|
|
3290
3290
|
}
|
|
3291
3291
|
|
|
3292
3292
|
.files-toolbar-actions button,
|
|
3293
|
-
.files-actions button
|
|
3293
|
+
.files-actions button,
|
|
3294
|
+
.files-sort-select {
|
|
3294
3295
|
padding: 4px 7px;
|
|
3295
3296
|
font-size: 11px;
|
|
3296
3297
|
}
|
|
3297
3298
|
|
|
3299
|
+
.files-sort-select {
|
|
3300
|
+
max-width: 150px;
|
|
3301
|
+
border: 1px solid var(--control-border);
|
|
3302
|
+
border-radius: 7px;
|
|
3303
|
+
background: var(--panel);
|
|
3304
|
+
color: var(--text);
|
|
3305
|
+
font-family: var(--font-ui);
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3298
3308
|
.files-subtitle,
|
|
3299
3309
|
.files-notice,
|
|
3300
3310
|
.files-empty {
|
package/index.ts
CHANGED
|
@@ -2752,6 +2752,7 @@ const STUDIO_FILE_BROWSER_IGNORED_DIRS = new Set([
|
|
|
2752
2752
|
]);
|
|
2753
2753
|
|
|
2754
2754
|
type StudioLocalPreviewResourceKind = "pdf" | "text" | "image" | "office" | "other";
|
|
2755
|
+
type StudioFileBrowserSortMode = "name" | "mtime-desc" | "mtime-asc" | "size-desc" | "size-asc";
|
|
2755
2756
|
|
|
2756
2757
|
interface StudioLocalPreviewResource {
|
|
2757
2758
|
filePath: string;
|
|
@@ -2773,6 +2774,29 @@ interface StudioFileBrowserEntry {
|
|
|
2773
2774
|
hidden: boolean;
|
|
2774
2775
|
}
|
|
2775
2776
|
|
|
2777
|
+
function normalizeStudioFileBrowserSortMode(sort: string | null | undefined): StudioFileBrowserSortMode {
|
|
2778
|
+
const value = typeof sort === "string" ? sort.trim().toLowerCase() : "";
|
|
2779
|
+
if (value === "mtime-desc" || value === "modified-desc" || value === "newest") return "mtime-desc";
|
|
2780
|
+
if (value === "mtime-asc" || value === "modified-asc" || value === "oldest") return "mtime-asc";
|
|
2781
|
+
if (value === "size-desc" || value === "largest") return "size-desc";
|
|
2782
|
+
if (value === "size-asc" || value === "smallest") return "size-asc";
|
|
2783
|
+
return "name";
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
function compareStudioFileBrowserEntryNames(a: StudioFileBrowserEntry, b: StudioFileBrowserEntry): number {
|
|
2787
|
+
return a.name.localeCompare(b.name, undefined, { sensitivity: "base", numeric: true });
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
function compareStudioFileBrowserEntries(a: StudioFileBrowserEntry, b: StudioFileBrowserEntry, sort: StudioFileBrowserSortMode): number {
|
|
2791
|
+
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
2792
|
+
if (a.hidden !== b.hidden) return a.hidden ? 1 : -1;
|
|
2793
|
+
if (sort === "mtime-desc") return (b.mtimeMs - a.mtimeMs) || compareStudioFileBrowserEntryNames(a, b);
|
|
2794
|
+
if (sort === "mtime-asc") return (a.mtimeMs - b.mtimeMs) || compareStudioFileBrowserEntryNames(a, b);
|
|
2795
|
+
if (sort === "size-desc" && a.type === "file") return (b.size - a.size) || compareStudioFileBrowserEntryNames(a, b);
|
|
2796
|
+
if (sort === "size-asc" && a.type === "file") return (a.size - b.size) || compareStudioFileBrowserEntryNames(a, b);
|
|
2797
|
+
return compareStudioFileBrowserEntryNames(a, b);
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2776
2800
|
function resolveStudioPdfResourcePath(pdfPath: string | undefined, sourcePath: string | undefined, resourceDir: string | undefined, fallbackCwd: string): string {
|
|
2777
2801
|
const rawPath = typeof pdfPath === "string" ? pdfPath.trim() : "";
|
|
2778
2802
|
if (!rawPath) throw new Error("Missing PDF path.");
|
|
@@ -2922,8 +2946,10 @@ function listStudioFileBrowserDirectory(
|
|
|
2922
2946
|
sourcePath: string | undefined,
|
|
2923
2947
|
resourceDir: string | undefined,
|
|
2924
2948
|
fallbackCwd: string,
|
|
2925
|
-
|
|
2949
|
+
sortMode?: string | null,
|
|
2950
|
+
): { rootDir: string; currentDir: string; relativeDir: string; parentDir: string | null; entries: StudioFileBrowserEntry[]; omitted: number; omittedIgnored: number; sort: StudioFileBrowserSortMode } {
|
|
2926
2951
|
const context = resolveStudioFileBrowserDirectory(dirPath, sourcePath, resourceDir, fallbackCwd);
|
|
2952
|
+
const sort = normalizeStudioFileBrowserSortMode(sortMode);
|
|
2927
2953
|
const entries: StudioFileBrowserEntry[] = [];
|
|
2928
2954
|
let omitted = 0;
|
|
2929
2955
|
let omittedIgnored = 0;
|
|
@@ -2962,14 +2988,10 @@ function listStudioFileBrowserDirectory(
|
|
|
2962
2988
|
omitted += 1;
|
|
2963
2989
|
}
|
|
2964
2990
|
}
|
|
2965
|
-
entries.sort((a, b) =>
|
|
2966
|
-
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
2967
|
-
if (a.hidden !== b.hidden) return a.hidden ? 1 : -1;
|
|
2968
|
-
return a.name.localeCompare(b.name, undefined, { sensitivity: "base", numeric: true });
|
|
2969
|
-
});
|
|
2991
|
+
entries.sort((a, b) => compareStudioFileBrowserEntries(a, b, sort));
|
|
2970
2992
|
const limitedEntries = entries.slice(0, STUDIO_FILE_BROWSER_MAX_ENTRIES);
|
|
2971
2993
|
omitted += Math.max(0, entries.length - limitedEntries.length);
|
|
2972
|
-
return { ...context, entries: limitedEntries, omitted, omittedIgnored };
|
|
2994
|
+
return { ...context, entries: limitedEntries, omitted, omittedIgnored, sort };
|
|
2973
2995
|
}
|
|
2974
2996
|
|
|
2975
2997
|
function resolveStudioHtmlPreviewResourcePath(
|
|
@@ -14150,6 +14172,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14150
14172
|
requestUrl.searchParams.get("sourcePath") ?? undefined,
|
|
14151
14173
|
requestUrl.searchParams.get("resourceDir") ?? undefined,
|
|
14152
14174
|
studioCwd,
|
|
14175
|
+
requestUrl.searchParams.get("sort") ?? undefined,
|
|
14153
14176
|
);
|
|
14154
14177
|
respondJson(res, 200, { ok: true, ...listing, entries: method === "HEAD" ? [] : listing.entries });
|
|
14155
14178
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.35",
|
|
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",
|