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 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
@@ -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
- ): { rootDir: string; currentDir: string; relativeDir: string; parentDir: string | null; entries: StudioFileBrowserEntry[]; omitted: number; omittedIgnored: number } {
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.34",
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",