pi-studio 0.9.33 → 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 +12 -0
- package/README.md +15 -5
- package/assets/demo/README.md +17 -0
- package/assets/demo/generated/gradient_descent_contour.png +0 -0
- package/assets/demo/generated/gradient_descent_convergence.png +0 -0
- package/assets/demo/gradient-descent-demo.md +80 -0
- package/assets/demo/gradient_descent_demo.py +88 -0
- package/assets/screenshots/dark-workspace.png +0 -0
- package/assets/screenshots/dark-zen.png +0 -0
- package/assets/screenshots/light-workspace.png +0 -0
- package/assets/screenshots/theme-zen-code.png +0 -0
- package/client/studio-client.js +256 -26
- package/client/studio.css +50 -9
- package/index.ts +110 -12
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,18 @@ 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
|
+
|
|
12
|
+
## [0.9.34] — 2026-07-02
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
- Added a bottom-right Studio theme menu for switching the active Pi theme without returning to the terminal.
|
|
16
|
+
- Added `Cmd/Ctrl+Alt+1–7` shortcuts for switching directly between all right-pane views, with editor-only views warning on unavailable full-session panes.
|
|
17
|
+
- Added refreshed README screenshots plus a reproducible gradient-descent screenshot fixture under `assets/demo/`.
|
|
18
|
+
|
|
7
19
|
## [0.9.33] — 2026-06-16
|
|
8
20
|
|
|
9
21
|
### Added
|
package/README.md
CHANGED
|
@@ -6,25 +6,35 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
|
|
|
6
6
|
|
|
7
7
|
[Watch the 2-minute demo (MP4, 2x speed, no audio)](https://github.com/omaclaren/pi-studio/releases/latest/download/pi-studio-demo-2min.mp4)
|
|
8
8
|
|
|
9
|
+
_The video shows an earlier version of the Studio interface. The basic workflow is the same, but there have been minor UI changes since it was recorded; the screenshots show the more recent version._
|
|
10
|
+
|
|
9
11
|
## Screenshots
|
|
10
12
|
|
|
11
|
-
**Dark**
|
|
13
|
+
**Dark workspace**
|
|
12
14
|
|
|
13
15
|

|
|
14
16
|
|
|
15
|
-
**Light**
|
|
17
|
+
**Light workspace**
|
|
16
18
|
|
|
17
19
|

|
|
18
20
|
|
|
21
|
+
**Zen mode**
|
|
22
|
+
|
|
23
|
+

|
|
24
|
+
|
|
25
|
+
**Zen mode with a custom theme and rendered code preview**
|
|
26
|
+
|
|
27
|
+

|
|
28
|
+
|
|
19
29
|
## What it does
|
|
20
30
|
|
|
21
31
|
- Opens a two-pane browser workspace: **Editor** (left) + **Response/Working/Editor Preview** (right)
|
|
22
32
|
- Supports one canonical full Studio view per Pi session, plus additional editor-only companion views when you want extra editing/preview surfaces; editor-only views can also browse files and use the Studio REPL send controls without taking over the full Studio session view
|
|
23
33
|
- Includes a global **Zen** mode for hiding secondary Studio chrome without changing the current left/right pane layout
|
|
24
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
|
|
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, and `Cmd/Ctrl+Alt+P` / `Cmd/Ctrl+Alt+E` / `Cmd/Ctrl+Alt+W`
|
|
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
|
|
26
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
|
|
27
|
-
- 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
|
|
28
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
|
|
29
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
|
|
30
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
|
|
@@ -46,7 +56,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
|
|
|
46
56
|
- Ships optional `pi-studio-dark` and `pi-studio-light` themes tuned for Studio's browser workspace
|
|
47
57
|
- Exports right-pane preview as PDF (pandoc + LaTeX) or standalone HTML into the source file directory, Studio working directory, or Pi session directory; PDF export can open in a Studio preview tab or the default PDF viewer, and HTML export can open in the default browser or in a new Studio editor tab for inspection/commenting, while preserving authored HTML previews as HTML and rendering CSV/TSV editor previews as tables
|
|
48
58
|
- Exports local files headlessly via `/studio-pdf <path>` to `<name>.studio.pdf` or `/studio-html <path>` to `<name>.studio.html`; without a path, those commands export the last model response to a timestamped file. Agent tools `studio_export_pdf` and `studio_export_html` expose the same export pipeline for remote/Telegram-style sessions.
|
|
49
|
-
- Shows model/session/context usage in the footer, plus
|
|
59
|
+
- Shows model/session/context usage in the footer, plus compact-context and active-theme controls
|
|
50
60
|
|
|
51
61
|
## Commands
|
|
52
62
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# pi Studio screenshot demo
|
|
2
|
+
|
|
3
|
+
This directory contains the source document and generated figures used for pi Studio README screenshots.
|
|
4
|
+
|
|
5
|
+
Regenerate figures from the repository root:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python3 assets/demo/gradient_descent_demo.py
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Open the Markdown fixture in Studio:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
/studio assets/demo/gradient-descent-demo.md
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Then switch the right pane to **Editor (Preview)** and capture dark/light screenshots as needed.
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Gradient descent on a non-convex surface
|
|
2
|
+
|
|
3
|
+
This is a compact **pi Studio** demo mixing prose, display math, generated figures, Python code, and inline annotations.
|
|
4
|
+
|
|
5
|
+
## 1. The objective
|
|
6
|
+
|
|
7
|
+
Consider the function
|
|
8
|
+
|
|
9
|
+
$$
|
|
10
|
+
f(x,y) = 0.35x^2 + 1.2y^2 + 0.8\sin(1.5x)\cos(2y).
|
|
11
|
+
$$
|
|
12
|
+
|
|
13
|
+
It is smooth, but not purely quadratic, so the sinusoidal term creates local ripples [an: define formally] in the loss landscape.
|
|
14
|
+
|
|
15
|
+
The gradient is
|
|
16
|
+
|
|
17
|
+
$$
|
|
18
|
+
\nabla f(x,y) =
|
|
19
|
+
\begin{pmatrix}
|
|
20
|
+
0.7x + 1.2\cos(1.5x)\cos(2y) \\
|
|
21
|
+
2.4y - 1.6\sin(1.5x)\sin(2y)
|
|
22
|
+
\end{pmatrix}.
|
|
23
|
+
$$
|
|
24
|
+
|
|
25
|
+
Starting from an initial point $p_0 = (x_0, y_0)$, gradient descent updates via
|
|
26
|
+
|
|
27
|
+
$$
|
|
28
|
+
p_{k+1} = p_k - \eta \, \nabla f(p_k),
|
|
29
|
+
$$
|
|
30
|
+
|
|
31
|
+
where $\eta > 0$ is the learning rate.
|
|
32
|
+
|
|
33
|
+
## 2. Optimization path in parameter space
|
|
34
|
+
|
|
35
|
+
The contour plot below shows the objective landscape together with the iterates of gradient descent. [an: did you actually run?]
|
|
36
|
+
|
|
37
|
+
{ width=420px }
|
|
38
|
+
|
|
39
|
+
The red path shows how the algorithm follows the local slope downhill. Because the surface is non-convex, the geometry is more interesting than a simple bowl-shaped quadratic.
|
|
40
|
+
|
|
41
|
+
## 3. Objective value by iteration
|
|
42
|
+
|
|
43
|
+
The next figure shows the scalar objective value $f(x_k,y_k)$ as the method runs.
|
|
44
|
+
|
|
45
|
+
{ width=400px }
|
|
46
|
+
|
|
47
|
+
A simple first-order method can still make steady progress when the step size is chosen reasonably.
|
|
48
|
+
|
|
49
|
+
## 4. Minimal Python snippet
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import numpy as np
|
|
53
|
+
|
|
54
|
+
def f(x, y):
|
|
55
|
+
return 0.35 * x**2 + 1.2 * y**2 + 0.8 * np.sin(1.5 * x) * np.cos(2.0 * y)
|
|
56
|
+
|
|
57
|
+
def grad(x, y):
|
|
58
|
+
dfdx = 0.7 * x + 1.2 * np.cos(1.5 * x) * np.cos(2.0 * y)
|
|
59
|
+
dfdy = 2.4 * y - 1.6 * np.sin(1.5 * x) * np.sin(2.0 * y)
|
|
60
|
+
return np.array([dfdx, dfdy])
|
|
61
|
+
|
|
62
|
+
p = np.array([-3.4, 2.8], dtype=float)
|
|
63
|
+
eta = 0.14
|
|
64
|
+
for _ in range(30):
|
|
65
|
+
p = p - eta * grad(*p)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## 5. Why this works well as a Studio demo
|
|
69
|
+
|
|
70
|
+
- the math is recognizable but not too heavy
|
|
71
|
+
- the figures are visually appealing
|
|
72
|
+
- the code is short and readable
|
|
73
|
+
- the preview shows equations, prose, code, images, annotations, and outline navigation together in one place
|
|
74
|
+
|
|
75
|
+
Demo assets:
|
|
76
|
+
|
|
77
|
+
- `assets/demo/gradient-descent-demo.md`
|
|
78
|
+
- `assets/demo/gradient_descent_demo.py`
|
|
79
|
+
- `assets/demo/generated/gradient_descent_contour.png`
|
|
80
|
+
- `assets/demo/generated/gradient_descent_convergence.png`
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate figures for the pi Studio README demo.
|
|
3
|
+
|
|
4
|
+
Run from the repository root:
|
|
5
|
+
|
|
6
|
+
python3 assets/demo/gradient_descent_demo.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import matplotlib.pyplot as plt
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
OUTPUT_DIR = Path(__file__).resolve().parent / "generated"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def f(x: np.ndarray | float, y: np.ndarray | float) -> np.ndarray | float:
|
|
21
|
+
return 0.35 * x**2 + 1.2 * y**2 + 0.8 * np.sin(1.5 * x) * np.cos(2.0 * y)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def grad(x: float, y: float) -> np.ndarray:
|
|
25
|
+
dfdx = 0.7 * x + 1.2 * np.cos(1.5 * x) * np.cos(2.0 * y)
|
|
26
|
+
dfdy = 2.4 * y - 1.6 * np.sin(1.5 * x) * np.sin(2.0 * y)
|
|
27
|
+
return np.array([dfdx, dfdy])
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def run_gradient_descent(start: tuple[float, float] = (-3.4, 2.8), eta: float = 0.14, steps: int = 30) -> np.ndarray:
|
|
31
|
+
path = [np.array(start, dtype=float)]
|
|
32
|
+
p = path[0].copy()
|
|
33
|
+
for _ in range(steps):
|
|
34
|
+
p = p - eta * grad(float(p[0]), float(p[1]))
|
|
35
|
+
path.append(p.copy())
|
|
36
|
+
return np.vstack(path)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def save_contour_plot(path: np.ndarray) -> None:
|
|
40
|
+
xs = np.linspace(-4.5, 4.5, 260)
|
|
41
|
+
ys = np.linspace(-3.4, 3.4, 220)
|
|
42
|
+
X, Y = np.meshgrid(xs, ys)
|
|
43
|
+
Z = f(X, Y)
|
|
44
|
+
|
|
45
|
+
fig, ax = plt.subplots(figsize=(6.2, 4.6), dpi=160)
|
|
46
|
+
levels = np.linspace(float(Z.min()), float(Z.max()), 34)
|
|
47
|
+
contour = ax.contourf(X, Y, Z, levels=levels, cmap="viridis")
|
|
48
|
+
ax.contour(X, Y, Z, levels=levels[::2], colors="white", linewidths=0.35, alpha=0.45)
|
|
49
|
+
ax.plot(path[:, 0], path[:, 1], color="#ff6b6b", linewidth=2.0, marker="o", markersize=2.2)
|
|
50
|
+
ax.scatter(path[0, 0], path[0, 1], s=36, color="#ffd166", edgecolor="black", linewidth=0.5, label="start", zorder=5)
|
|
51
|
+
ax.scatter(path[-1, 0], path[-1, 1], s=36, color="#06d6a0", edgecolor="black", linewidth=0.5, label="final", zorder=5)
|
|
52
|
+
ax.set_title("Gradient descent on a non-convex surface", fontsize=10)
|
|
53
|
+
ax.set_xlabel("x")
|
|
54
|
+
ax.set_ylabel("y")
|
|
55
|
+
ax.legend(loc="upper right", frameon=True, fontsize=7)
|
|
56
|
+
fig.colorbar(contour, ax=ax, label="f(x, y)")
|
|
57
|
+
fig.tight_layout()
|
|
58
|
+
fig.savefig(OUTPUT_DIR / "gradient_descent_contour.png", bbox_inches="tight")
|
|
59
|
+
plt.close(fig)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def save_convergence_plot(path: np.ndarray) -> None:
|
|
63
|
+
values = np.array([f(float(x), float(y)) for x, y in path])
|
|
64
|
+
iterations = np.arange(len(values))
|
|
65
|
+
|
|
66
|
+
fig, ax = plt.subplots(figsize=(5.8, 3.4), dpi=160)
|
|
67
|
+
ax.plot(iterations, values, color="#2a9d8f", linewidth=2.2, marker="o", markersize=3)
|
|
68
|
+
ax.set_title("Objective value by iteration", fontsize=10)
|
|
69
|
+
ax.set_xlabel("iteration")
|
|
70
|
+
ax.set_ylabel("f(x_k, y_k)")
|
|
71
|
+
ax.grid(True, color="#d0d7de", linewidth=0.7, alpha=0.8)
|
|
72
|
+
ax.margins(x=0.02)
|
|
73
|
+
fig.tight_layout()
|
|
74
|
+
fig.savefig(OUTPUT_DIR / "gradient_descent_convergence.png", bbox_inches="tight")
|
|
75
|
+
plt.close(fig)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def main() -> None:
|
|
79
|
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
path = run_gradient_descent()
|
|
81
|
+
save_contour_plot(path)
|
|
82
|
+
save_convergence_plot(path)
|
|
83
|
+
print(f"Wrote {OUTPUT_DIR / 'gradient_descent_contour.png'}")
|
|
84
|
+
print(f"Wrote {OUTPUT_DIR / 'gradient_descent_convergence.png'}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
if __name__ == "__main__":
|
|
88
|
+
main()
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/client/studio-client.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
const footerMetaTerminalEl = document.getElementById("footerMetaTerminal");
|
|
9
9
|
const footerMetaContextEl = document.getElementById("footerMetaContext");
|
|
10
10
|
const footerModelMenuEl = document.getElementById("footerModelMenu");
|
|
11
|
+
const footerThemeBtn = document.getElementById("footerThemeBtn");
|
|
12
|
+
const footerThemeMenuEl = document.getElementById("footerThemeMenu");
|
|
11
13
|
let faviconLinkEl = document.querySelector('link[rel="icon"], link[rel="shortcut icon"]');
|
|
12
14
|
if (!faviconLinkEl) {
|
|
13
15
|
faviconLinkEl = document.createElement("link");
|
|
@@ -178,6 +180,32 @@
|
|
|
178
180
|
: "full";
|
|
179
181
|
const isEditorOnlyMode = studioMode === "editor-only";
|
|
180
182
|
const isSshStudioSession = Boolean(document.body && document.body.dataset && document.body.dataset.sshSession === "1");
|
|
183
|
+
const EDITOR_ONLY_RIGHT_VIEW_ALLOWED = new Set(["editor-preview", "files", "changes", "repl"]);
|
|
184
|
+
const RIGHT_VIEW_LABELS = {
|
|
185
|
+
markdown: "Response (Raw)",
|
|
186
|
+
preview: "Response (Preview)",
|
|
187
|
+
"editor-preview": "Editor (Preview)",
|
|
188
|
+
trace: "Working",
|
|
189
|
+
changes: "Changes",
|
|
190
|
+
files: "Files",
|
|
191
|
+
repl: "REPL",
|
|
192
|
+
};
|
|
193
|
+
const RIGHT_VIEW_NUMERIC_SHORTCUTS = {
|
|
194
|
+
Digit1: "markdown",
|
|
195
|
+
Digit2: "preview",
|
|
196
|
+
Digit3: "editor-preview",
|
|
197
|
+
Digit4: "trace",
|
|
198
|
+
Digit5: "changes",
|
|
199
|
+
Digit6: "files",
|
|
200
|
+
Digit7: "repl",
|
|
201
|
+
Numpad1: "markdown",
|
|
202
|
+
Numpad2: "preview",
|
|
203
|
+
Numpad3: "editor-preview",
|
|
204
|
+
Numpad4: "trace",
|
|
205
|
+
Numpad5: "changes",
|
|
206
|
+
Numpad6: "files",
|
|
207
|
+
Numpad7: "repl",
|
|
208
|
+
};
|
|
181
209
|
|
|
182
210
|
const initialQueryParams = new URLSearchParams(window.location.search || "");
|
|
183
211
|
const skipInitialWorkspaceRestore = initialQueryParams.get("skipWorkspaceRestore") === "1";
|
|
@@ -242,9 +270,9 @@
|
|
|
242
270
|
let sourceOpenCurrentTextCopyTabBtn = null;
|
|
243
271
|
let sourceSessionSummaryEl = null;
|
|
244
272
|
let initialDocumentApplied = false;
|
|
245
|
-
function
|
|
273
|
+
function canonicalRightViewValue(nextView) {
|
|
246
274
|
const raw = String(nextView || "").trim();
|
|
247
|
-
|
|
275
|
+
return raw === "preview"
|
|
248
276
|
? "preview"
|
|
249
277
|
: (raw === "editor-preview"
|
|
250
278
|
? "editor-preview"
|
|
@@ -255,22 +283,39 @@
|
|
|
255
283
|
: (raw === "changes"
|
|
256
284
|
? "changes"
|
|
257
285
|
: ((raw === "trace" || raw === "thinking") ? "trace" : "markdown")))));
|
|
258
|
-
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function normalizeRightViewValue(nextView) {
|
|
289
|
+
const normalized = canonicalRightViewValue(nextView);
|
|
290
|
+
if (isEditorOnlyMode && !EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(normalized)) {
|
|
259
291
|
return "editor-preview";
|
|
260
292
|
}
|
|
261
293
|
return normalized;
|
|
262
294
|
}
|
|
263
295
|
|
|
296
|
+
function isRightViewAvailableInCurrentMode(view) {
|
|
297
|
+
const normalized = canonicalRightViewValue(view);
|
|
298
|
+
return !isEditorOnlyMode || EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(normalized);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function getRightViewDisplayLabel(view) {
|
|
302
|
+
const normalized = canonicalRightViewValue(view);
|
|
303
|
+
if (rightViewSelect && rightViewSelect.options) {
|
|
304
|
+
const option = Array.from(rightViewSelect.options).find((candidate) => candidate && candidate.value === normalized);
|
|
305
|
+
if (option && option.textContent) return option.textContent;
|
|
306
|
+
}
|
|
307
|
+
return RIGHT_VIEW_LABELS[normalized] || normalized;
|
|
308
|
+
}
|
|
309
|
+
|
|
264
310
|
function syncRightViewModeOptions() {
|
|
265
311
|
if (!rightViewSelect || !rightViewSelect.options) return;
|
|
266
|
-
const editorOnlyAllowed = new Set(["editor-preview", "files", "changes", "repl"]);
|
|
267
312
|
Array.from(rightViewSelect.options).forEach((option) => {
|
|
268
313
|
if (!option) return;
|
|
269
|
-
option.disabled = isEditorOnlyMode && !
|
|
314
|
+
option.disabled = isEditorOnlyMode && !EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(option.value);
|
|
270
315
|
});
|
|
271
316
|
rightViewSelect.title = isEditorOnlyMode
|
|
272
|
-
? "Editor-only views:
|
|
273
|
-
: "Right pane view mode. F7 cycles
|
|
317
|
+
? "Editor-only views: Editor Preview, Changes, Files, or REPL. F7 cycles; Cmd/Ctrl+Alt+3/5/6/7 switch directly to available right-pane views."
|
|
318
|
+
: "Right pane view mode. F7 cycles; Cmd/Ctrl+Alt+1–7 switches directly between all right-pane views. Cmd/Ctrl+Alt+P/E/W keep their mnemonic Preview/Editor Preview/Working shortcuts.";
|
|
274
319
|
}
|
|
275
320
|
|
|
276
321
|
function getInitialRightView(source) {
|
|
@@ -528,6 +573,9 @@
|
|
|
528
573
|
let piCurrentModel = null;
|
|
529
574
|
let piThinkingLevel = "";
|
|
530
575
|
let footerModelMenuOpen = false;
|
|
576
|
+
let piThemeOptions = [];
|
|
577
|
+
let piCurrentTheme = (document.body && document.body.dataset && document.body.dataset.themeName) || "";
|
|
578
|
+
let footerThemeMenuOpen = false;
|
|
531
579
|
let terminalSessionLabel = (document.body && document.body.dataset && document.body.dataset.terminalLabel) || "unknown";
|
|
532
580
|
let terminalSessionDetail = (document.body && document.body.dataset && document.body.dataset.terminalDetail) || terminalSessionLabel;
|
|
533
581
|
let contextTokens = null;
|
|
@@ -2056,6 +2104,35 @@
|
|
|
2056
2104
|
const DEFAULT_RESPONSE_FONT_SIZE = studioUiRefreshEnabled ? 13.5 : 15;
|
|
2057
2105
|
let editorFontSize = DEFAULT_EDITOR_FONT_SIZE;
|
|
2058
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();
|
|
2059
2136
|
let fileBrowserState = {
|
|
2060
2137
|
rootDir: "",
|
|
2061
2138
|
currentDir: "",
|
|
@@ -2064,6 +2141,7 @@
|
|
|
2064
2141
|
entries: [],
|
|
2065
2142
|
omitted: 0,
|
|
2066
2143
|
omittedIgnored: 0,
|
|
2144
|
+
sort: fileBrowserSortMode,
|
|
2067
2145
|
loading: false,
|
|
2068
2146
|
error: "",
|
|
2069
2147
|
loaded: false,
|
|
@@ -3273,6 +3351,7 @@
|
|
|
3273
3351
|
|
|
3274
3352
|
function setFooterModelMenuOpen(open) {
|
|
3275
3353
|
footerModelMenuOpen = Boolean(open);
|
|
3354
|
+
if (footerModelMenuOpen) setFooterThemeMenuOpen(false);
|
|
3276
3355
|
if (footerModelMenuEl) footerModelMenuEl.hidden = !footerModelMenuOpen;
|
|
3277
3356
|
if (footerMetaModelEl) {
|
|
3278
3357
|
footerMetaModelEl.classList.toggle("is-open", footerModelMenuOpen);
|
|
@@ -3298,6 +3377,80 @@
|
|
|
3298
3377
|
if (sent) setStatus("Setting Pi thinking level…", "warning");
|
|
3299
3378
|
}
|
|
3300
3379
|
|
|
3380
|
+
function normalizePiThemeOptions(options) {
|
|
3381
|
+
return Array.isArray(options)
|
|
3382
|
+
? options.map((option) => ({
|
|
3383
|
+
name: String(option && option.name || "").trim(),
|
|
3384
|
+
path: String(option && option.path || "").trim(),
|
|
3385
|
+
})).filter((option) => option.name)
|
|
3386
|
+
: [];
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
function updatePiThemeState(message) {
|
|
3390
|
+
if (!message || typeof message !== "object") return;
|
|
3391
|
+
if (Array.isArray(message.piThemes)) {
|
|
3392
|
+
piThemeOptions = normalizePiThemeOptions(message.piThemes);
|
|
3393
|
+
}
|
|
3394
|
+
if (message.currentTheme && typeof message.currentTheme === "object") {
|
|
3395
|
+
const name = String(message.currentTheme.name || "").trim();
|
|
3396
|
+
if (name) piCurrentTheme = name;
|
|
3397
|
+
} else if (typeof message.currentTheme === "string" && message.currentTheme.trim()) {
|
|
3398
|
+
piCurrentTheme = message.currentTheme.trim();
|
|
3399
|
+
}
|
|
3400
|
+
updateFooterThemeButton();
|
|
3401
|
+
renderFooterThemeMenu();
|
|
3402
|
+
}
|
|
3403
|
+
|
|
3404
|
+
function updateFooterThemeButton() {
|
|
3405
|
+
if (!footerThemeBtn) return;
|
|
3406
|
+
const label = piCurrentTheme && piCurrentTheme.trim() ? piCurrentTheme.trim() : "theme";
|
|
3407
|
+
footerThemeBtn.textContent = "Theme: " + label;
|
|
3408
|
+
footerThemeBtn.title = "Switch the active Pi theme. Current theme: " + label + ". This affects terminal Pi and Studio.";
|
|
3409
|
+
footerThemeBtn.setAttribute("aria-expanded", footerThemeMenuOpen ? "true" : "false");
|
|
3410
|
+
}
|
|
3411
|
+
|
|
3412
|
+
function renderFooterThemeMenu() {
|
|
3413
|
+
if (!footerThemeMenuEl) return;
|
|
3414
|
+
const current = piCurrentTheme && piCurrentTheme.trim() ? piCurrentTheme.trim() : "";
|
|
3415
|
+
const optionNames = new Set(piThemeOptions.map((option) => option.name));
|
|
3416
|
+
const options = piThemeOptions.slice();
|
|
3417
|
+
if (current && !optionNames.has(current)) {
|
|
3418
|
+
options.unshift({ name: current, path: "" });
|
|
3419
|
+
}
|
|
3420
|
+
const optionsHtml = options.map((option) => {
|
|
3421
|
+
const selected = option.name === current ? " selected" : "";
|
|
3422
|
+
const title = option.path ? " title='" + escapeHtml(option.path) + "'" : "";
|
|
3423
|
+
return "<option value='" + escapeHtml(option.name) + "'" + selected + title + ">" + escapeHtml(option.name) + "</option>";
|
|
3424
|
+
});
|
|
3425
|
+
footerThemeMenuEl.innerHTML = ""
|
|
3426
|
+
+ "<div class='footer-model-menu-heading'>Pi theme</div>"
|
|
3427
|
+
+ (optionsHtml.length
|
|
3428
|
+
? "<label class='footer-model-menu-field'><span>Active theme</span><select id='footerPiThemeSelect'>" + optionsHtml.join("") + "</select></label>"
|
|
3429
|
+
: "<div class='footer-model-menu-note'>No Pi themes are available yet.</div>")
|
|
3430
|
+
+ "<div class='footer-model-menu-note'>Switches the active Pi theme and persists it to Pi settings.</div>";
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
function setFooterThemeMenuOpen(open) {
|
|
3434
|
+
footerThemeMenuOpen = Boolean(open);
|
|
3435
|
+
if (footerThemeMenuOpen) setFooterModelMenuOpen(false);
|
|
3436
|
+
if (footerThemeMenuEl) footerThemeMenuEl.hidden = !footerThemeMenuOpen;
|
|
3437
|
+
if (footerThemeBtn) {
|
|
3438
|
+
footerThemeBtn.classList.toggle("is-open", footerThemeMenuOpen);
|
|
3439
|
+
footerThemeBtn.setAttribute("aria-expanded", footerThemeMenuOpen ? "true" : "false");
|
|
3440
|
+
}
|
|
3441
|
+
if (footerThemeMenuOpen) renderFooterThemeMenu();
|
|
3442
|
+
}
|
|
3443
|
+
|
|
3444
|
+
function requestPiThemeSelection(themeName) {
|
|
3445
|
+
const normalized = String(themeName || "").trim();
|
|
3446
|
+
if (!normalized) {
|
|
3447
|
+
setStatus("No Pi theme selected.", "warning");
|
|
3448
|
+
return;
|
|
3449
|
+
}
|
|
3450
|
+
const sent = sendMessage({ type: "pi_theme_select_request", theme: normalized });
|
|
3451
|
+
if (sent) setStatus("Switching Pi theme…", "warning");
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3301
3454
|
function updateFooterMeta() {
|
|
3302
3455
|
const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
|
|
3303
3456
|
const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
|
|
@@ -3795,33 +3948,33 @@
|
|
|
3795
3948
|
setStatus("Right pane content focused.");
|
|
3796
3949
|
}
|
|
3797
3950
|
|
|
3798
|
-
function
|
|
3799
|
-
const
|
|
3951
|
+
function switchRightPaneToView(nextView, options) {
|
|
3952
|
+
const requestedView = canonicalRightViewValue(nextView);
|
|
3953
|
+
const label = getRightViewDisplayLabel(requestedView);
|
|
3954
|
+
if (!isRightViewAvailableInCurrentMode(requestedView)) {
|
|
3955
|
+
setStatus(label + " is unavailable in editor-only Studio views.", "warning");
|
|
3956
|
+
return false;
|
|
3957
|
+
}
|
|
3800
3958
|
const snapshot = snapshotStudioScrollablePositions();
|
|
3801
|
-
setRightView(
|
|
3959
|
+
setRightView(requestedView);
|
|
3802
3960
|
scheduleStudioScrollablePositionRestore(snapshot);
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3961
|
+
if (!options || options.announce !== false) {
|
|
3962
|
+
setStatus("Right pane view: " + label + ".");
|
|
3963
|
+
}
|
|
3964
|
+
return true;
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
function switchRightPaneToPrimaryPreview() {
|
|
3968
|
+
const targetView = isEditorOnlyMode ? "editor-preview" : "preview";
|
|
3969
|
+
switchRightPaneToView(targetView);
|
|
3807
3970
|
}
|
|
3808
3971
|
|
|
3809
3972
|
function switchRightPaneToWorking() {
|
|
3810
|
-
|
|
3811
|
-
setStatus("Working view is unavailable in editor-only Studio views.", "warning");
|
|
3812
|
-
return;
|
|
3813
|
-
}
|
|
3814
|
-
const snapshot = snapshotStudioScrollablePositions();
|
|
3815
|
-
setRightView("trace");
|
|
3816
|
-
scheduleStudioScrollablePositionRestore(snapshot);
|
|
3817
|
-
setStatus("Right pane view: Working.");
|
|
3973
|
+
switchRightPaneToView("trace");
|
|
3818
3974
|
}
|
|
3819
3975
|
|
|
3820
3976
|
function switchRightPaneToEditorPreview() {
|
|
3821
|
-
|
|
3822
|
-
setRightView("editor-preview");
|
|
3823
|
-
scheduleStudioScrollablePositionRestore(snapshot);
|
|
3824
|
-
setStatus("Right pane view: Editor (Preview).");
|
|
3977
|
+
switchRightPaneToView("editor-preview");
|
|
3825
3978
|
}
|
|
3826
3979
|
|
|
3827
3980
|
function cycleActivePaneView(direction) {
|
|
@@ -4122,6 +4275,17 @@
|
|
|
4122
4275
|
return;
|
|
4123
4276
|
}
|
|
4124
4277
|
|
|
4278
|
+
const numericRightViewShortcut = (event.metaKey || event.ctrlKey)
|
|
4279
|
+
&& event.altKey
|
|
4280
|
+
&& !event.shiftKey
|
|
4281
|
+
? (RIGHT_VIEW_NUMERIC_SHORTCUTS[code] || RIGHT_VIEW_NUMERIC_SHORTCUTS["Digit" + key])
|
|
4282
|
+
: null;
|
|
4283
|
+
if (numericRightViewShortcut) {
|
|
4284
|
+
event.preventDefault();
|
|
4285
|
+
switchRightPaneToView(numericRightViewShortcut);
|
|
4286
|
+
return;
|
|
4287
|
+
}
|
|
4288
|
+
|
|
4125
4289
|
const isPreviewShortcut = (key.toLowerCase() === "p" || code === "KeyP")
|
|
4126
4290
|
&& (event.metaKey || event.ctrlKey)
|
|
4127
4291
|
&& event.altKey
|
|
@@ -8011,6 +8175,9 @@
|
|
|
8011
8175
|
critiqueViewEl.addEventListener("click", handleFilesPaneClick);
|
|
8012
8176
|
critiqueViewEl.addEventListener("click", handleGitChangesPaneClick);
|
|
8013
8177
|
critiqueViewEl.addEventListener("change", handleReplPaneChange);
|
|
8178
|
+
critiqueViewEl.addEventListener("change", (event) => {
|
|
8179
|
+
void handleFilesPaneChange(event);
|
|
8180
|
+
});
|
|
8014
8181
|
}
|
|
8015
8182
|
|
|
8016
8183
|
function replaceResponsePaneWithClone() {
|
|
@@ -9831,6 +9998,24 @@
|
|
|
9831
9998
|
return entry.extension ? entry.extension.replace(/^\./, "") : "file";
|
|
9832
9999
|
}
|
|
9833
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
|
+
|
|
9834
10019
|
function buildFileBrowserPanelHtml() {
|
|
9835
10020
|
const state = fileBrowserState || {};
|
|
9836
10021
|
const entries = Array.isArray(state.entries) ? state.entries : [];
|
|
@@ -9885,6 +10070,7 @@
|
|
|
9885
10070
|
+ "<div class='files-toolbar'>"
|
|
9886
10071
|
+ "<div class='files-path-group'><span class='files-label'>Files</span><span class='files-path' title='" + escapeHtml(currentDir) + "'>" + escapeHtml(relativeDir || ".") + "</span></div>"
|
|
9887
10072
|
+ "<div class='files-toolbar-actions'>"
|
|
10073
|
+
+ buildFileBrowserSortSelectHtml()
|
|
9888
10074
|
+ "<button type='button' data-files-action='parent'" + parentDisabled + ">Parent</button>"
|
|
9889
10075
|
+ "<button type='button' data-files-action='refresh'>Refresh</button>"
|
|
9890
10076
|
+ (currentDir ? "<button type='button' data-files-action='copy-current' data-files-path='" + escapeHtml(currentDir) + "'>Copy path</button>" : "")
|
|
@@ -9911,6 +10097,7 @@
|
|
|
9911
10097
|
entries: [],
|
|
9912
10098
|
omitted: 0,
|
|
9913
10099
|
omittedIgnored: 0,
|
|
10100
|
+
sort: fileBrowserSortMode,
|
|
9914
10101
|
loading: false,
|
|
9915
10102
|
error: "",
|
|
9916
10103
|
loaded: false,
|
|
@@ -9945,8 +10132,10 @@
|
|
|
9945
10132
|
if (dir) query.dir = String(dir);
|
|
9946
10133
|
if (context.sourcePath) query.sourcePath = context.sourcePath;
|
|
9947
10134
|
if (context.resourceDir) query.resourceDir = context.resourceDir;
|
|
10135
|
+
query.sort = normalizeFileBrowserSortMode(fileBrowserSortMode);
|
|
9948
10136
|
const payload = await fetchStudioJson("/file-browser", { query });
|
|
9949
10137
|
if (nonce !== fileBrowserLoadNonce) return;
|
|
10138
|
+
fileBrowserSortMode = normalizeFileBrowserSortMode(payload.sort || fileBrowserSortMode);
|
|
9950
10139
|
fileBrowserState = {
|
|
9951
10140
|
rootDir: typeof payload.rootDir === "string" ? payload.rootDir : "",
|
|
9952
10141
|
currentDir: typeof payload.currentDir === "string" ? payload.currentDir : "",
|
|
@@ -9955,6 +10144,7 @@
|
|
|
9955
10144
|
entries: Array.isArray(payload.entries) ? payload.entries : [],
|
|
9956
10145
|
omitted: Number(payload.omitted) || 0,
|
|
9957
10146
|
omittedIgnored: Number(payload.omittedIgnored) || 0,
|
|
10147
|
+
sort: normalizeFileBrowserSortMode(payload.sort || fileBrowserSortMode),
|
|
9958
10148
|
loading: false,
|
|
9959
10149
|
error: "",
|
|
9960
10150
|
loaded: true,
|
|
@@ -10057,6 +10247,17 @@
|
|
|
10057
10247
|
setStatus(payload && payload.message ? payload.message : "Opened folder in file manager.", "success");
|
|
10058
10248
|
}
|
|
10059
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
|
+
|
|
10060
10261
|
async function handleFilesPaneClick(event) {
|
|
10061
10262
|
if (rightView !== "files") return;
|
|
10062
10263
|
const target = event.target;
|
|
@@ -18827,6 +19028,7 @@
|
|
|
18827
19028
|
updateCompletionSuggestionModelOptions(message.suggestionModels);
|
|
18828
19029
|
}
|
|
18829
19030
|
updatePiSessionModelState(message);
|
|
19031
|
+
updatePiThemeState(message);
|
|
18830
19032
|
if (typeof message.terminalSessionLabel === "string") {
|
|
18831
19033
|
terminalSessionLabel = message.terminalSessionLabel;
|
|
18832
19034
|
}
|
|
@@ -19403,6 +19605,7 @@
|
|
|
19403
19605
|
updateCompletionSuggestionModelOptions(message.suggestionModels);
|
|
19404
19606
|
}
|
|
19405
19607
|
updatePiSessionModelState(message);
|
|
19608
|
+
updatePiThemeState(message);
|
|
19406
19609
|
if (typeof message.terminalSessionLabel === "string") {
|
|
19407
19610
|
terminalSessionLabel = message.terminalSessionLabel;
|
|
19408
19611
|
}
|
|
@@ -19536,6 +19739,7 @@
|
|
|
19536
19739
|
root.style.setProperty(key, message.vars[key]);
|
|
19537
19740
|
}
|
|
19538
19741
|
});
|
|
19742
|
+
updatePiThemeState(message);
|
|
19539
19743
|
updateDocumentTitle();
|
|
19540
19744
|
}
|
|
19541
19745
|
}
|
|
@@ -20283,6 +20487,7 @@
|
|
|
20283
20487
|
closeExportPreviewMenu();
|
|
20284
20488
|
closePreviewLinkMenu();
|
|
20285
20489
|
setFooterModelMenuOpen(false);
|
|
20490
|
+
setFooterThemeMenuOpen(false);
|
|
20286
20491
|
}
|
|
20287
20492
|
});
|
|
20288
20493
|
|
|
@@ -20309,11 +20514,36 @@
|
|
|
20309
20514
|
}
|
|
20310
20515
|
});
|
|
20311
20516
|
}
|
|
20517
|
+
if (footerThemeBtn) {
|
|
20518
|
+
footerThemeBtn.addEventListener("click", (event) => {
|
|
20519
|
+
event.preventDefault();
|
|
20520
|
+
event.stopPropagation();
|
|
20521
|
+
setFooterThemeMenuOpen(!footerThemeMenuOpen);
|
|
20522
|
+
});
|
|
20523
|
+
}
|
|
20524
|
+
if (footerThemeMenuEl) {
|
|
20525
|
+
footerThemeMenuEl.addEventListener("click", (event) => {
|
|
20526
|
+
event.stopPropagation();
|
|
20527
|
+
});
|
|
20528
|
+
footerThemeMenuEl.addEventListener("change", (event) => {
|
|
20529
|
+
const target = event.target;
|
|
20530
|
+
if (!(target instanceof HTMLSelectElement)) return;
|
|
20531
|
+
if (target.id === "footerPiThemeSelect") {
|
|
20532
|
+
requestPiThemeSelection(target.value);
|
|
20533
|
+
setFooterThemeMenuOpen(false);
|
|
20534
|
+
}
|
|
20535
|
+
});
|
|
20536
|
+
}
|
|
20312
20537
|
document.addEventListener("click", (event) => {
|
|
20313
20538
|
const target = event.target;
|
|
20314
20539
|
if (target instanceof Element && (target.closest("#footerModelMenu") || target.closest("#footerMetaModel"))) return;
|
|
20315
20540
|
setFooterModelMenuOpen(false);
|
|
20316
20541
|
});
|
|
20542
|
+
document.addEventListener("click", (event) => {
|
|
20543
|
+
const target = event.target;
|
|
20544
|
+
if (target instanceof Element && (target.closest("#footerThemeMenu") || target.closest("#footerThemeBtn"))) return;
|
|
20545
|
+
setFooterThemeMenuOpen(false);
|
|
20546
|
+
});
|
|
20317
20547
|
|
|
20318
20548
|
saveAsBtn.addEventListener("click", () => {
|
|
20319
20549
|
const content = sourceTextEl.value;
|
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 {
|
|
@@ -4277,11 +4287,10 @@
|
|
|
4277
4287
|
outline: none;
|
|
4278
4288
|
}
|
|
4279
4289
|
|
|
4280
|
-
.footer-model-menu
|
|
4290
|
+
.footer-model-menu,
|
|
4291
|
+
.footer-theme-menu {
|
|
4281
4292
|
position: fixed;
|
|
4282
|
-
left: 12px;
|
|
4283
4293
|
bottom: 38px;
|
|
4284
|
-
width: min(440px, calc(100vw - 24px));
|
|
4285
4294
|
padding: 10px;
|
|
4286
4295
|
border: 1px solid var(--panel-border);
|
|
4287
4296
|
border-radius: 12px;
|
|
@@ -4291,7 +4300,18 @@
|
|
|
4291
4300
|
z-index: 110;
|
|
4292
4301
|
}
|
|
4293
4302
|
|
|
4294
|
-
.footer-model-menu
|
|
4303
|
+
.footer-model-menu {
|
|
4304
|
+
left: 12px;
|
|
4305
|
+
width: min(440px, calc(100vw - 24px));
|
|
4306
|
+
}
|
|
4307
|
+
|
|
4308
|
+
.footer-theme-menu {
|
|
4309
|
+
right: 12px;
|
|
4310
|
+
width: min(320px, calc(100vw - 24px));
|
|
4311
|
+
}
|
|
4312
|
+
|
|
4313
|
+
.footer-model-menu[hidden],
|
|
4314
|
+
.footer-theme-menu[hidden] {
|
|
4295
4315
|
display: none !important;
|
|
4296
4316
|
}
|
|
4297
4317
|
|
|
@@ -4348,10 +4368,18 @@
|
|
|
4348
4368
|
opacity: 0.72;
|
|
4349
4369
|
}
|
|
4350
4370
|
|
|
4351
|
-
.
|
|
4371
|
+
.footer-actions {
|
|
4352
4372
|
grid-area: hint;
|
|
4353
4373
|
justify-self: end;
|
|
4354
4374
|
align-self: center;
|
|
4375
|
+
display: inline-flex;
|
|
4376
|
+
align-items: center;
|
|
4377
|
+
justify-content: flex-end;
|
|
4378
|
+
gap: 6px;
|
|
4379
|
+
min-width: 0;
|
|
4380
|
+
}
|
|
4381
|
+
|
|
4382
|
+
.shortcut-hint {
|
|
4355
4383
|
display: inline-flex;
|
|
4356
4384
|
align-items: center;
|
|
4357
4385
|
gap: 6px;
|
|
@@ -4366,12 +4394,21 @@
|
|
|
4366
4394
|
opacity: 0.9;
|
|
4367
4395
|
}
|
|
4368
4396
|
|
|
4397
|
+
.footer-theme-btn {
|
|
4398
|
+
max-width: min(28ch, 36vw);
|
|
4399
|
+
min-width: 0;
|
|
4400
|
+
overflow: hidden;
|
|
4401
|
+
text-overflow: ellipsis;
|
|
4402
|
+
}
|
|
4403
|
+
|
|
4369
4404
|
.shortcut-hint:not(:disabled):hover,
|
|
4370
|
-
.shortcut-hint:focus-visible
|
|
4405
|
+
.shortcut-hint:focus-visible,
|
|
4406
|
+
.shortcut-hint.is-open {
|
|
4371
4407
|
background: var(--panel-2);
|
|
4372
4408
|
border-color: var(--control-border);
|
|
4373
4409
|
color: var(--text);
|
|
4374
4410
|
opacity: 1;
|
|
4411
|
+
outline: none;
|
|
4375
4412
|
}
|
|
4376
4413
|
|
|
4377
4414
|
.footer-compact-btn {
|
|
@@ -4398,7 +4435,7 @@
|
|
|
4398
4435
|
}
|
|
4399
4436
|
|
|
4400
4437
|
body.studio-zen-mode .footer-meta,
|
|
4401
|
-
body.studio-zen-mode .
|
|
4438
|
+
body.studio-zen-mode .footer-actions {
|
|
4402
4439
|
display: none !important;
|
|
4403
4440
|
}
|
|
4404
4441
|
|
|
@@ -4993,8 +5030,12 @@
|
|
|
4993
5030
|
max-width: 100%;
|
|
4994
5031
|
}
|
|
4995
5032
|
|
|
4996
|
-
.
|
|
5033
|
+
.footer-actions {
|
|
4997
5034
|
justify-self: start;
|
|
5035
|
+
flex-wrap: wrap;
|
|
5036
|
+
}
|
|
5037
|
+
|
|
5038
|
+
.shortcut-hint {
|
|
4998
5039
|
text-align: left;
|
|
4999
5040
|
white-space: normal;
|
|
5000
5041
|
flex-wrap: wrap;
|
package/index.ts
CHANGED
|
@@ -361,6 +361,11 @@ interface PiThinkingLevelRequestMessage {
|
|
|
361
361
|
level: ModelThinkingLevel;
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
interface PiThemeSelectRequestMessage {
|
|
365
|
+
type: "pi_theme_select_request";
|
|
366
|
+
theme: string;
|
|
367
|
+
}
|
|
368
|
+
|
|
364
369
|
interface QuizGenerateRequestMessage {
|
|
365
370
|
type: "quiz_generate_request";
|
|
366
371
|
requestId: string;
|
|
@@ -506,6 +511,7 @@ type IncomingStudioMessage =
|
|
|
506
511
|
| CompletionSuggestionCancelRequestMessage
|
|
507
512
|
| PiModelSelectRequestMessage
|
|
508
513
|
| PiThinkingLevelRequestMessage
|
|
514
|
+
| PiThemeSelectRequestMessage
|
|
509
515
|
| QuizGenerateRequestMessage
|
|
510
516
|
| QuizAnswerRequestMessage
|
|
511
517
|
| QuizDiscussRequestMessage
|
|
@@ -2746,6 +2752,7 @@ const STUDIO_FILE_BROWSER_IGNORED_DIRS = new Set([
|
|
|
2746
2752
|
]);
|
|
2747
2753
|
|
|
2748
2754
|
type StudioLocalPreviewResourceKind = "pdf" | "text" | "image" | "office" | "other";
|
|
2755
|
+
type StudioFileBrowserSortMode = "name" | "mtime-desc" | "mtime-asc" | "size-desc" | "size-asc";
|
|
2749
2756
|
|
|
2750
2757
|
interface StudioLocalPreviewResource {
|
|
2751
2758
|
filePath: string;
|
|
@@ -2767,6 +2774,29 @@ interface StudioFileBrowserEntry {
|
|
|
2767
2774
|
hidden: boolean;
|
|
2768
2775
|
}
|
|
2769
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
|
+
|
|
2770
2800
|
function resolveStudioPdfResourcePath(pdfPath: string | undefined, sourcePath: string | undefined, resourceDir: string | undefined, fallbackCwd: string): string {
|
|
2771
2801
|
const rawPath = typeof pdfPath === "string" ? pdfPath.trim() : "";
|
|
2772
2802
|
if (!rawPath) throw new Error("Missing PDF path.");
|
|
@@ -2916,8 +2946,10 @@ function listStudioFileBrowserDirectory(
|
|
|
2916
2946
|
sourcePath: string | undefined,
|
|
2917
2947
|
resourceDir: string | undefined,
|
|
2918
2948
|
fallbackCwd: string,
|
|
2919
|
-
|
|
2949
|
+
sortMode?: string | null,
|
|
2950
|
+
): { rootDir: string; currentDir: string; relativeDir: string; parentDir: string | null; entries: StudioFileBrowserEntry[]; omitted: number; omittedIgnored: number; sort: StudioFileBrowserSortMode } {
|
|
2920
2951
|
const context = resolveStudioFileBrowserDirectory(dirPath, sourcePath, resourceDir, fallbackCwd);
|
|
2952
|
+
const sort = normalizeStudioFileBrowserSortMode(sortMode);
|
|
2921
2953
|
const entries: StudioFileBrowserEntry[] = [];
|
|
2922
2954
|
let omitted = 0;
|
|
2923
2955
|
let omittedIgnored = 0;
|
|
@@ -2956,14 +2988,10 @@ function listStudioFileBrowserDirectory(
|
|
|
2956
2988
|
omitted += 1;
|
|
2957
2989
|
}
|
|
2958
2990
|
}
|
|
2959
|
-
entries.sort((a, b) =>
|
|
2960
|
-
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
2961
|
-
if (a.hidden !== b.hidden) return a.hidden ? 1 : -1;
|
|
2962
|
-
return a.name.localeCompare(b.name, undefined, { sensitivity: "base", numeric: true });
|
|
2963
|
-
});
|
|
2991
|
+
entries.sort((a, b) => compareStudioFileBrowserEntries(a, b, sort));
|
|
2964
2992
|
const limitedEntries = entries.slice(0, STUDIO_FILE_BROWSER_MAX_ENTRIES);
|
|
2965
2993
|
omitted += Math.max(0, entries.length - limitedEntries.length);
|
|
2966
|
-
return { ...context, entries: limitedEntries, omitted, omittedIgnored };
|
|
2994
|
+
return { ...context, entries: limitedEntries, omitted, omittedIgnored, sort };
|
|
2967
2995
|
}
|
|
2968
2996
|
|
|
2969
2997
|
function resolveStudioHtmlPreviewResourcePath(
|
|
@@ -8425,6 +8453,16 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
8425
8453
|
}
|
|
8426
8454
|
}
|
|
8427
8455
|
|
|
8456
|
+
if (msg.type === "pi_theme_select_request" && typeof msg.theme === "string") {
|
|
8457
|
+
const theme = msg.theme.trim();
|
|
8458
|
+
if (theme && theme.length <= 200) {
|
|
8459
|
+
return {
|
|
8460
|
+
type: "pi_theme_select_request",
|
|
8461
|
+
theme,
|
|
8462
|
+
};
|
|
8463
|
+
}
|
|
8464
|
+
}
|
|
8465
|
+
|
|
8428
8466
|
if (msg.type === "completion_suggestion_request" && typeof msg.requestId === "string" && typeof msg.text === "string") {
|
|
8429
8467
|
const textLength = msg.text.length;
|
|
8430
8468
|
const rawStart = typeof msg.selectionStart === "number" && Number.isFinite(msg.selectionStart) ? msg.selectionStart : textLength;
|
|
@@ -10277,6 +10315,7 @@ function buildStudioHtml(
|
|
|
10277
10315
|
const initialModel = escapeHtmlForInline(initialModelLabel ?? "none");
|
|
10278
10316
|
const initialTerminal = escapeHtmlForInline(initialTerminalLabel ?? "unknown");
|
|
10279
10317
|
const initialTerminalDetailAttr = escapeHtmlForInline(initialTerminalDetail ?? initialTerminalLabel ?? "unknown");
|
|
10318
|
+
const initialTheme = escapeHtmlForInline(theme?.name ?? "theme");
|
|
10280
10319
|
const initialContextTokens =
|
|
10281
10320
|
typeof initialContextUsage?.tokens === "number" && Number.isFinite(initialContextUsage.tokens)
|
|
10282
10321
|
? String(initialContextUsage.tokens)
|
|
@@ -10344,7 +10383,7 @@ ${cssVarsBlock}
|
|
|
10344
10383
|
</style>
|
|
10345
10384
|
<link rel="stylesheet" href="${stylesheetHref}" />
|
|
10346
10385
|
</head>
|
|
10347
|
-
<body data-initial-source="${initialSource}" data-initial-label="${initialLabel}" data-initial-path="${initialPath}" data-initial-draft-id="${initialDraftId}" data-initial-resource-dir="${initialResourceDir}" data-model-label="${initialModel}" data-terminal-label="${initialTerminal}" data-terminal-detail="${initialTerminalDetailAttr}" data-context-tokens="${initialContextTokens}" data-context-window="${initialContextWindow}" data-context-percent="${initialContextPercent}" data-studio-mode="${studioMode}" data-ssh-session="${initialSshSession}">
|
|
10386
|
+
<body data-initial-source="${initialSource}" data-initial-label="${initialLabel}" data-initial-path="${initialPath}" data-initial-draft-id="${initialDraftId}" data-initial-resource-dir="${initialResourceDir}" data-model-label="${initialModel}" data-terminal-label="${initialTerminal}" data-terminal-detail="${initialTerminalDetailAttr}" data-theme-name="${initialTheme}" data-context-tokens="${initialContextTokens}" data-context-window="${initialContextWindow}" data-context-percent="${initialContextPercent}" data-studio-mode="${studioMode}" data-ssh-session="${initialSshSession}">
|
|
10348
10387
|
<header>
|
|
10349
10388
|
<h1><span class="app-logo" aria-hidden="true">π</span> Studio <span class="app-subtitle">${appSubtitle}</span></h1>
|
|
10350
10389
|
<div class="controls">
|
|
@@ -10561,7 +10600,7 @@ ${cssVarsBlock}
|
|
|
10561
10600
|
<section id="rightPane">
|
|
10562
10601
|
<div id="rightSectionHeader" class="section-header">
|
|
10563
10602
|
<div class="section-header-main">
|
|
10564
|
-
<select id="rightViewSelect" aria-label="Response view mode" title="Right pane view mode. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+
|
|
10603
|
+
<select id="rightViewSelect" aria-label="Response view mode" title="Right pane view mode. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+1–7 switches directly between all right-pane views. Cmd/Ctrl+Alt+P/E/W keep their mnemonic Preview/Editor Preview/Working shortcuts.">
|
|
10565
10604
|
<option value="markdown">Response (Raw)</option>
|
|
10566
10605
|
<option value="preview" selected>Response (Preview)</option>
|
|
10567
10606
|
<option value="editor-preview">Editor (Preview)</option>
|
|
@@ -10637,7 +10676,11 @@ ${cssVarsBlock}
|
|
|
10637
10676
|
<span id="statusLine"><span id="statusSpinner" aria-hidden="true"> </span><span id="status">Booting studio…</span></span>
|
|
10638
10677
|
<span id="footerMeta" class="footer-meta"><span id="footerMetaText" class="footer-meta-text"><button id="footerMetaModel" class="footer-meta-part footer-meta-model footer-model-btn" type="button" aria-haspopup="menu" aria-expanded="false">${initialModel}</button><span class="footer-meta-sep">·</span><span id="footerMetaTerminal" class="footer-meta-part footer-meta-terminal">${initialTerminal}</span><span class="footer-meta-sep">·</span><span id="footerMetaContext" class="footer-meta-part footer-meta-context">unknown</span></span><button id="compactBtn" class="footer-compact-btn" type="button" title="Trigger pi context compaction now.">Compact</button></span>
|
|
10639
10678
|
<div id="footerModelMenu" class="footer-model-menu" hidden></div>
|
|
10640
|
-
<
|
|
10679
|
+
<div id="footerThemeMenu" class="footer-theme-menu" hidden></div>
|
|
10680
|
+
<span class="footer-actions">
|
|
10681
|
+
<button id="footerThemeBtn" class="shortcut-hint footer-theme-btn" type="button" aria-haspopup="menu" aria-expanded="false" title="Switch the active Pi theme. This affects terminal Pi and Studio.">Theme: ${initialTheme}</button>
|
|
10682
|
+
<button id="shortcutsBtn" class="shortcut-hint" type="button" title="Show Studio keyboard shortcuts. Press ? when not editing text.">Shortcuts (?)</button>
|
|
10683
|
+
</span>
|
|
10641
10684
|
</footer>
|
|
10642
10685
|
|
|
10643
10686
|
<div id="shortcutsOverlay" class="shortcuts-overlay" hidden>
|
|
@@ -10655,7 +10698,8 @@ ${cssVarsBlock}
|
|
|
10655
10698
|
<dl>
|
|
10656
10699
|
<div><dt>F6</dt><dd>Switch between editor and right pane</dd></div>
|
|
10657
10700
|
<div><dt>F7 / Shift+F7</dt><dd>Cycle the active pane's view</dd></div>
|
|
10658
|
-
<div><dt>Cmd/Ctrl+Alt+
|
|
10701
|
+
<div><dt>Cmd/Ctrl+Alt+1–7</dt><dd>Switch the right pane directly: Response Raw, Response Preview, Editor Preview, Working, Changes, Files, REPL</dd></div>
|
|
10702
|
+
<div><dt>Cmd/Ctrl+Alt+P</dt><dd>Switch the right pane directly to Response Preview; in editor-only views, Editor Preview</dd></div>
|
|
10659
10703
|
<div><dt>Cmd/Ctrl+Alt+E</dt><dd>Switch the right pane directly to Editor Preview</dd></div>
|
|
10660
10704
|
<div><dt>Cmd/Ctrl+Alt+W</dt><dd>Switch the right pane directly to Working</dd></div>
|
|
10661
10705
|
<div><dt>F8</dt><dd>Focus editor text</dd></div>
|
|
@@ -11770,6 +11814,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
11770
11814
|
}
|
|
11771
11815
|
: null;
|
|
11772
11816
|
|
|
11817
|
+
const getStudioThemeOptions = () => {
|
|
11818
|
+
try {
|
|
11819
|
+
const themes = lastCommandCtx?.ui?.getAllThemes?.() ?? [];
|
|
11820
|
+
return themes
|
|
11821
|
+
.map((theme) => ({
|
|
11822
|
+
name: typeof theme.name === "string" ? theme.name.trim() : "",
|
|
11823
|
+
path: typeof theme.path === "string" ? theme.path : undefined,
|
|
11824
|
+
}))
|
|
11825
|
+
.filter((theme) => theme.name);
|
|
11826
|
+
} catch {
|
|
11827
|
+
return [];
|
|
11828
|
+
}
|
|
11829
|
+
};
|
|
11830
|
+
|
|
11831
|
+
const getCurrentStudioThemeDescriptor = () => {
|
|
11832
|
+
const name = typeof lastCommandCtx?.ui?.theme?.name === "string" ? lastCommandCtx.ui.theme.name.trim() : "";
|
|
11833
|
+
return name ? { name } : null;
|
|
11834
|
+
};
|
|
11835
|
+
|
|
11773
11836
|
const broadcastState = () => {
|
|
11774
11837
|
terminalSessionLabel = buildTerminalSessionLabel(studioCwd, getSessionNameSafe());
|
|
11775
11838
|
terminalSessionDetail = buildTerminalSessionDetail(studioCwd, getSessionNameSafe());
|
|
@@ -11788,6 +11851,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
11788
11851
|
thinkingLevel: getThinkingLevelSafe() ?? "off",
|
|
11789
11852
|
piModels: modelOptions,
|
|
11790
11853
|
suggestionModels: modelOptions,
|
|
11854
|
+
currentTheme: getCurrentStudioThemeDescriptor(),
|
|
11855
|
+
piThemes: getStudioThemeOptions(),
|
|
11791
11856
|
terminalSessionLabel,
|
|
11792
11857
|
terminalSessionDetail,
|
|
11793
11858
|
contextTokens: contextUsageSnapshot.tokens,
|
|
@@ -12087,6 +12152,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
12087
12152
|
thinkingLevel: getThinkingLevelSafe() ?? "off",
|
|
12088
12153
|
piModels: getStudioModelOptions(),
|
|
12089
12154
|
suggestionModels: getStudioModelOptions(),
|
|
12155
|
+
currentTheme: getCurrentStudioThemeDescriptor(),
|
|
12156
|
+
piThemes: getStudioThemeOptions(),
|
|
12090
12157
|
terminalSessionLabel,
|
|
12091
12158
|
terminalSessionDetail,
|
|
12092
12159
|
contextTokens: contextUsageSnapshot.tokens,
|
|
@@ -12146,6 +12213,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
12146
12213
|
return;
|
|
12147
12214
|
}
|
|
12148
12215
|
|
|
12216
|
+
if (msg.type === "pi_theme_select_request") {
|
|
12217
|
+
const ui = lastCommandCtx?.ui;
|
|
12218
|
+
if (!ui || typeof ui.setTheme !== "function") {
|
|
12219
|
+
sendToClient(client, { type: "info", level: "warning", message: "Pi theme controls are not available yet." });
|
|
12220
|
+
return;
|
|
12221
|
+
}
|
|
12222
|
+
const themeName = msg.theme.trim();
|
|
12223
|
+
const availableThemes = getStudioThemeOptions();
|
|
12224
|
+
if (availableThemes.length && !availableThemes.some((theme) => theme.name === themeName)) {
|
|
12225
|
+
sendToClient(client, { type: "info", level: "warning", message: `Pi theme not found: ${themeName}` });
|
|
12226
|
+
return;
|
|
12227
|
+
}
|
|
12228
|
+
try {
|
|
12229
|
+
const result = ui.setTheme(themeName);
|
|
12230
|
+
if (!result.success) {
|
|
12231
|
+
sendToClient(client, { type: "info", level: "error", message: `Theme switch failed: ${result.error ?? themeName}` });
|
|
12232
|
+
return;
|
|
12233
|
+
}
|
|
12234
|
+
const vars = buildThemeCssVars(getStudioThemeStyle(ui.theme));
|
|
12235
|
+
lastThemeVarsJson = JSON.stringify(vars);
|
|
12236
|
+
syncCmuxStudioStatus();
|
|
12237
|
+
broadcast({ type: "theme_update", vars, currentTheme: getCurrentStudioThemeDescriptor(), piThemes: getStudioThemeOptions() });
|
|
12238
|
+
broadcastState();
|
|
12239
|
+
sendToClient(client, { type: "info", level: "info", message: `Pi theme switched to ${themeName}.` });
|
|
12240
|
+
} catch (error) {
|
|
12241
|
+
sendToClient(client, { type: "info", level: "error", message: `Theme switch failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
12242
|
+
}
|
|
12243
|
+
return;
|
|
12244
|
+
}
|
|
12245
|
+
|
|
12149
12246
|
if (msg.type === "get_latest_response") {
|
|
12150
12247
|
if (!lastStudioResponse) {
|
|
12151
12248
|
sendToClient(client, { type: "info", message: "No latest assistant response is available yet." });
|
|
@@ -14075,6 +14172,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14075
14172
|
requestUrl.searchParams.get("sourcePath") ?? undefined,
|
|
14076
14173
|
requestUrl.searchParams.get("resourceDir") ?? undefined,
|
|
14077
14174
|
studioCwd,
|
|
14175
|
+
requestUrl.searchParams.get("sort") ?? undefined,
|
|
14078
14176
|
);
|
|
14079
14177
|
respondJson(res, 200, { ok: true, ...listing, entries: method === "HEAD" ? [] : listing.entries });
|
|
14080
14178
|
} catch (error) {
|
|
@@ -14335,7 +14433,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14335
14433
|
lastThemeVarsJson = json;
|
|
14336
14434
|
syncCmuxStudioStatus();
|
|
14337
14435
|
for (const client of serverState.clients) {
|
|
14338
|
-
sendToClient(client, { type: "theme_update", vars });
|
|
14436
|
+
sendToClient(client, { type: "theme_update", vars, currentTheme: getCurrentStudioThemeDescriptor(), piThemes: getStudioThemeOptions() });
|
|
14339
14437
|
}
|
|
14340
14438
|
}
|
|
14341
14439
|
} catch {
|
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",
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"README.md",
|
|
26
26
|
"CHANGELOG.md",
|
|
27
27
|
"WORKFLOW.md",
|
|
28
|
-
"assets/screenshots"
|
|
28
|
+
"assets/screenshots",
|
|
29
|
+
"assets/demo"
|
|
29
30
|
],
|
|
30
31
|
"scripts": {
|
|
31
32
|
"test": "node --test",
|