pi-studio 0.9.34 → 0.9.36

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,16 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.9.36] — 2026-07-13
8
+
9
+ ### Changed
10
+ - Added `max` to the Studio footer Pi thinking selector to match newer Pi thinking levels.
11
+
12
+ ## [0.9.35] — 2026-07-13
13
+
14
+ ### Added
15
+ - Added Files-view sort options for name, newest/oldest modified time, and largest/smallest file size.
16
+
7
17
  ## [0.9.34] — 2026-07-02
8
18
 
9
19
  ### 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,
@@ -3292,7 +3322,7 @@
3292
3322
  }
3293
3323
 
3294
3324
  function getPiThinkingLevels() {
3295
- return ["off", "minimal", "low", "medium", "high", "xhigh"];
3325
+ return ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
3296
3326
  }
3297
3327
 
3298
3328
  function renderFooterModelMenu() {
@@ -3309,7 +3339,9 @@
3309
3339
  modelOptionsHtml.unshift("<option value='" + escapeHtml(currentValue) + "' selected>" + escapeHtml(label || "current model") + "</option>");
3310
3340
  }
3311
3341
  const thinking = piThinkingLevel || "off";
3312
- const thinkingOptionsHtml = getPiThinkingLevels().map((level) => {
3342
+ const thinkingLevels = getPiThinkingLevels();
3343
+ if (thinking && !thinkingLevels.includes(thinking)) thinkingLevels.push(thinking);
3344
+ const thinkingOptionsHtml = thinkingLevels.map((level) => {
3313
3345
  return "<option value='" + escapeHtml(level) + "'" + (level === thinking ? " selected" : "") + ">Thinking: " + escapeHtml(level) + "</option>";
3314
3346
  });
3315
3347
  footerModelMenuEl.innerHTML = ""
@@ -8145,6 +8177,9 @@
8145
8177
  critiqueViewEl.addEventListener("click", handleFilesPaneClick);
8146
8178
  critiqueViewEl.addEventListener("click", handleGitChangesPaneClick);
8147
8179
  critiqueViewEl.addEventListener("change", handleReplPaneChange);
8180
+ critiqueViewEl.addEventListener("change", (event) => {
8181
+ void handleFilesPaneChange(event);
8182
+ });
8148
8183
  }
8149
8184
 
8150
8185
  function replaceResponsePaneWithClone() {
@@ -9965,6 +10000,24 @@
9965
10000
  return entry.extension ? entry.extension.replace(/^\./, "") : "file";
9966
10001
  }
9967
10002
 
10003
+ function getFileBrowserSortOptions() {
10004
+ return [
10005
+ { value: "name", label: "Sort: Name" },
10006
+ { value: "mtime-desc", label: "Sort: Newest" },
10007
+ { value: "mtime-asc", label: "Sort: Oldest" },
10008
+ { value: "size-desc", label: "Sort: Largest" },
10009
+ { value: "size-asc", label: "Sort: Smallest" },
10010
+ ];
10011
+ }
10012
+
10013
+ function buildFileBrowserSortSelectHtml() {
10014
+ const currentSort = normalizeFileBrowserSortMode(fileBrowserSortMode || (fileBrowserState && fileBrowserState.sort));
10015
+ const options = getFileBrowserSortOptions().map((option) => {
10016
+ return "<option value='" + escapeHtml(option.value) + "'" + (option.value === currentSort ? " selected" : "") + ">" + escapeHtml(option.label) + "</option>";
10017
+ }).join("");
10018
+ 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>";
10019
+ }
10020
+
9968
10021
  function buildFileBrowserPanelHtml() {
9969
10022
  const state = fileBrowserState || {};
9970
10023
  const entries = Array.isArray(state.entries) ? state.entries : [];
@@ -10019,6 +10072,7 @@
10019
10072
  + "<div class='files-toolbar'>"
10020
10073
  + "<div class='files-path-group'><span class='files-label'>Files</span><span class='files-path' title='" + escapeHtml(currentDir) + "'>" + escapeHtml(relativeDir || ".") + "</span></div>"
10021
10074
  + "<div class='files-toolbar-actions'>"
10075
+ + buildFileBrowserSortSelectHtml()
10022
10076
  + "<button type='button' data-files-action='parent'" + parentDisabled + ">Parent</button>"
10023
10077
  + "<button type='button' data-files-action='refresh'>Refresh</button>"
10024
10078
  + (currentDir ? "<button type='button' data-files-action='copy-current' data-files-path='" + escapeHtml(currentDir) + "'>Copy path</button>" : "")
@@ -10045,6 +10099,7 @@
10045
10099
  entries: [],
10046
10100
  omitted: 0,
10047
10101
  omittedIgnored: 0,
10102
+ sort: fileBrowserSortMode,
10048
10103
  loading: false,
10049
10104
  error: "",
10050
10105
  loaded: false,
@@ -10079,8 +10134,10 @@
10079
10134
  if (dir) query.dir = String(dir);
10080
10135
  if (context.sourcePath) query.sourcePath = context.sourcePath;
10081
10136
  if (context.resourceDir) query.resourceDir = context.resourceDir;
10137
+ query.sort = normalizeFileBrowserSortMode(fileBrowserSortMode);
10082
10138
  const payload = await fetchStudioJson("/file-browser", { query });
10083
10139
  if (nonce !== fileBrowserLoadNonce) return;
10140
+ fileBrowserSortMode = normalizeFileBrowserSortMode(payload.sort || fileBrowserSortMode);
10084
10141
  fileBrowserState = {
10085
10142
  rootDir: typeof payload.rootDir === "string" ? payload.rootDir : "",
10086
10143
  currentDir: typeof payload.currentDir === "string" ? payload.currentDir : "",
@@ -10089,6 +10146,7 @@
10089
10146
  entries: Array.isArray(payload.entries) ? payload.entries : [],
10090
10147
  omitted: Number(payload.omitted) || 0,
10091
10148
  omittedIgnored: Number(payload.omittedIgnored) || 0,
10149
+ sort: normalizeFileBrowserSortMode(payload.sort || fileBrowserSortMode),
10092
10150
  loading: false,
10093
10151
  error: "",
10094
10152
  loaded: true,
@@ -10191,6 +10249,17 @@
10191
10249
  setStatus(payload && payload.message ? payload.message : "Opened folder in file manager.", "success");
10192
10250
  }
10193
10251
 
10252
+ async function handleFilesPaneChange(event) {
10253
+ if (rightView !== "files") return;
10254
+ const target = event.target;
10255
+ const sortSelect = target instanceof Element ? target.closest("[data-files-sort]") : null;
10256
+ if (!sortSelect || !("value" in sortSelect)) return;
10257
+ const nextSort = writeFileBrowserSortMode(sortSelect.value);
10258
+ if (nextSort === fileBrowserSortMode && fileBrowserState.loaded && !fileBrowserState.loading) return;
10259
+ fileBrowserSortMode = nextSort;
10260
+ await loadFileBrowserDirectory(fileBrowserState.currentDir || "", { user: true });
10261
+ }
10262
+
10194
10263
  async function handleFilesPaneClick(event) {
10195
10264
  if (rightView !== "files") return;
10196
10265
  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
@@ -44,6 +44,7 @@ type StudioReplRuntime = "shell" | "python" | "ipython" | "julia" | "r" | "ghci"
44
44
  type StudioQuizAngle = "general" | "scientist" | "mathematician" | "statistician" | "developer" | "reviewer";
45
45
  type StudioQuizScope = "selection" | "editor" | "file" | "folder" | "repo";
46
46
  type StudioQuizThinking = "off" | "minimal" | "low" | "medium" | "high";
47
+ type StudioPiThinkingLevel = ModelThinkingLevel | "max";
47
48
 
48
49
  const STUDIO_CSS_URL = new URL("./client/studio.css", import.meta.url);
49
50
  const STUDIO_ANNOTATION_HELPERS_URL = new URL("./client/studio-annotation-helpers.js", import.meta.url);
@@ -358,7 +359,7 @@ interface PiModelSelectRequestMessage {
358
359
 
359
360
  interface PiThinkingLevelRequestMessage {
360
361
  type: "pi_thinking_level_request";
361
- level: ModelThinkingLevel;
362
+ level: StudioPiThinkingLevel;
362
363
  }
363
364
 
364
365
  interface PiThemeSelectRequestMessage {
@@ -2752,6 +2753,7 @@ const STUDIO_FILE_BROWSER_IGNORED_DIRS = new Set([
2752
2753
  ]);
2753
2754
 
2754
2755
  type StudioLocalPreviewResourceKind = "pdf" | "text" | "image" | "office" | "other";
2756
+ type StudioFileBrowserSortMode = "name" | "mtime-desc" | "mtime-asc" | "size-desc" | "size-asc";
2755
2757
 
2756
2758
  interface StudioLocalPreviewResource {
2757
2759
  filePath: string;
@@ -2773,6 +2775,29 @@ interface StudioFileBrowserEntry {
2773
2775
  hidden: boolean;
2774
2776
  }
2775
2777
 
2778
+ function normalizeStudioFileBrowserSortMode(sort: string | null | undefined): StudioFileBrowserSortMode {
2779
+ const value = typeof sort === "string" ? sort.trim().toLowerCase() : "";
2780
+ if (value === "mtime-desc" || value === "modified-desc" || value === "newest") return "mtime-desc";
2781
+ if (value === "mtime-asc" || value === "modified-asc" || value === "oldest") return "mtime-asc";
2782
+ if (value === "size-desc" || value === "largest") return "size-desc";
2783
+ if (value === "size-asc" || value === "smallest") return "size-asc";
2784
+ return "name";
2785
+ }
2786
+
2787
+ function compareStudioFileBrowserEntryNames(a: StudioFileBrowserEntry, b: StudioFileBrowserEntry): number {
2788
+ return a.name.localeCompare(b.name, undefined, { sensitivity: "base", numeric: true });
2789
+ }
2790
+
2791
+ function compareStudioFileBrowserEntries(a: StudioFileBrowserEntry, b: StudioFileBrowserEntry, sort: StudioFileBrowserSortMode): number {
2792
+ if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
2793
+ if (a.hidden !== b.hidden) return a.hidden ? 1 : -1;
2794
+ if (sort === "mtime-desc") return (b.mtimeMs - a.mtimeMs) || compareStudioFileBrowserEntryNames(a, b);
2795
+ if (sort === "mtime-asc") return (a.mtimeMs - b.mtimeMs) || compareStudioFileBrowserEntryNames(a, b);
2796
+ if (sort === "size-desc" && a.type === "file") return (b.size - a.size) || compareStudioFileBrowserEntryNames(a, b);
2797
+ if (sort === "size-asc" && a.type === "file") return (a.size - b.size) || compareStudioFileBrowserEntryNames(a, b);
2798
+ return compareStudioFileBrowserEntryNames(a, b);
2799
+ }
2800
+
2776
2801
  function resolveStudioPdfResourcePath(pdfPath: string | undefined, sourcePath: string | undefined, resourceDir: string | undefined, fallbackCwd: string): string {
2777
2802
  const rawPath = typeof pdfPath === "string" ? pdfPath.trim() : "";
2778
2803
  if (!rawPath) throw new Error("Missing PDF path.");
@@ -2922,8 +2947,10 @@ function listStudioFileBrowserDirectory(
2922
2947
  sourcePath: string | undefined,
2923
2948
  resourceDir: string | undefined,
2924
2949
  fallbackCwd: string,
2925
- ): { rootDir: string; currentDir: string; relativeDir: string; parentDir: string | null; entries: StudioFileBrowserEntry[]; omitted: number; omittedIgnored: number } {
2950
+ sortMode?: string | null,
2951
+ ): { rootDir: string; currentDir: string; relativeDir: string; parentDir: string | null; entries: StudioFileBrowserEntry[]; omitted: number; omittedIgnored: number; sort: StudioFileBrowserSortMode } {
2926
2952
  const context = resolveStudioFileBrowserDirectory(dirPath, sourcePath, resourceDir, fallbackCwd);
2953
+ const sort = normalizeStudioFileBrowserSortMode(sortMode);
2927
2954
  const entries: StudioFileBrowserEntry[] = [];
2928
2955
  let omitted = 0;
2929
2956
  let omittedIgnored = 0;
@@ -2962,14 +2989,10 @@ function listStudioFileBrowserDirectory(
2962
2989
  omitted += 1;
2963
2990
  }
2964
2991
  }
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
- });
2992
+ entries.sort((a, b) => compareStudioFileBrowserEntries(a, b, sort));
2970
2993
  const limitedEntries = entries.slice(0, STUDIO_FILE_BROWSER_MAX_ENTRIES);
2971
2994
  omitted += Math.max(0, entries.length - limitedEntries.length);
2972
- return { ...context, entries: limitedEntries, omitted, omittedIgnored };
2995
+ return { ...context, entries: limitedEntries, omitted, omittedIgnored, sort };
2973
2996
  }
2974
2997
 
2975
2998
  function resolveStudioHtmlPreviewResourcePath(
@@ -8423,7 +8446,7 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
8423
8446
 
8424
8447
  if (msg.type === "pi_thinking_level_request" && typeof msg.level === "string") {
8425
8448
  const level = msg.level.trim().toLowerCase();
8426
- if (level === "off" || level === "minimal" || level === "low" || level === "medium" || level === "high" || level === "xhigh") {
8449
+ if (level === "off" || level === "minimal" || level === "low" || level === "medium" || level === "high" || level === "xhigh" || level === "max") {
8427
8450
  return {
8428
8451
  type: "pi_thinking_level_request",
8429
8452
  level,
@@ -11054,17 +11077,17 @@ export default function (pi: ExtensionAPI) {
11054
11077
  }
11055
11078
  };
11056
11079
 
11057
- const getThinkingLevelSafe = (): ModelThinkingLevel | undefined => {
11080
+ const getThinkingLevelSafe = (): StudioPiThinkingLevel | undefined => {
11058
11081
  try {
11059
- return pi.getThinkingLevel() as ModelThinkingLevel;
11082
+ return pi.getThinkingLevel() as StudioPiThinkingLevel;
11060
11083
  } catch {
11061
11084
  return undefined;
11062
11085
  }
11063
11086
  };
11064
11087
 
11065
- const setThinkingLevelSafe = (level: ModelThinkingLevel) => {
11066
- // Pi's CLI/model config support "off" as a thinking level; some extension API typings still expose the narrower reasoning-only type.
11067
- (pi.setThinkingLevel as (nextLevel: ModelThinkingLevel) => void)(level);
11088
+ const setThinkingLevelSafe = (level: StudioPiThinkingLevel) => {
11089
+ // Pi's CLI/model config support "off" and newer levels such as "max"; some extension API typings still expose a narrower reasoning-only type.
11090
+ (pi.setThinkingLevel as (nextLevel: StudioPiThinkingLevel) => void)(level);
11068
11091
  };
11069
11092
 
11070
11093
  const refreshRuntimeMetadata = (ctx?: { cwd?: string; model?: { provider?: string; id?: string; name?: string; reasoning?: boolean } | undefined }) => {
@@ -14150,6 +14173,7 @@ export default function (pi: ExtensionAPI) {
14150
14173
  requestUrl.searchParams.get("sourcePath") ?? undefined,
14151
14174
  requestUrl.searchParams.get("resourceDir") ?? undefined,
14152
14175
  studioCwd,
14176
+ requestUrl.searchParams.get("sort") ?? undefined,
14153
14177
  );
14154
14178
  respondJson(res, 200, { ok: true, ...listing, entries: method === "HEAD" ? [] : listing.entries });
14155
14179
  } 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.36",
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",