pi-studio 0.9.33 → 0.9.34
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 +7 -0
- package/README.md +14 -4
- 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 +189 -26
- package/client/studio.css +39 -8
- package/index.ts +80 -5
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.9.34] — 2026-07-02
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Added a bottom-right Studio theme menu for switching the active Pi theme without returning to the terminal.
|
|
11
|
+
- 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.
|
|
12
|
+
- Added refreshed README screenshots plus a reproducible gradient-descent screenshot fixture under `assets/demo/`.
|
|
13
|
+
|
|
7
14
|
## [0.9.33] — 2026-06-16
|
|
8
15
|
|
|
9
16
|
### Added
|
package/README.md
CHANGED
|
@@ -6,23 +6,33 @@ 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
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
|
|
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
|
|
@@ -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;
|
|
@@ -3273,6 +3321,7 @@
|
|
|
3273
3321
|
|
|
3274
3322
|
function setFooterModelMenuOpen(open) {
|
|
3275
3323
|
footerModelMenuOpen = Boolean(open);
|
|
3324
|
+
if (footerModelMenuOpen) setFooterThemeMenuOpen(false);
|
|
3276
3325
|
if (footerModelMenuEl) footerModelMenuEl.hidden = !footerModelMenuOpen;
|
|
3277
3326
|
if (footerMetaModelEl) {
|
|
3278
3327
|
footerMetaModelEl.classList.toggle("is-open", footerModelMenuOpen);
|
|
@@ -3298,6 +3347,80 @@
|
|
|
3298
3347
|
if (sent) setStatus("Setting Pi thinking level…", "warning");
|
|
3299
3348
|
}
|
|
3300
3349
|
|
|
3350
|
+
function normalizePiThemeOptions(options) {
|
|
3351
|
+
return Array.isArray(options)
|
|
3352
|
+
? options.map((option) => ({
|
|
3353
|
+
name: String(option && option.name || "").trim(),
|
|
3354
|
+
path: String(option && option.path || "").trim(),
|
|
3355
|
+
})).filter((option) => option.name)
|
|
3356
|
+
: [];
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
function updatePiThemeState(message) {
|
|
3360
|
+
if (!message || typeof message !== "object") return;
|
|
3361
|
+
if (Array.isArray(message.piThemes)) {
|
|
3362
|
+
piThemeOptions = normalizePiThemeOptions(message.piThemes);
|
|
3363
|
+
}
|
|
3364
|
+
if (message.currentTheme && typeof message.currentTheme === "object") {
|
|
3365
|
+
const name = String(message.currentTheme.name || "").trim();
|
|
3366
|
+
if (name) piCurrentTheme = name;
|
|
3367
|
+
} else if (typeof message.currentTheme === "string" && message.currentTheme.trim()) {
|
|
3368
|
+
piCurrentTheme = message.currentTheme.trim();
|
|
3369
|
+
}
|
|
3370
|
+
updateFooterThemeButton();
|
|
3371
|
+
renderFooterThemeMenu();
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
function updateFooterThemeButton() {
|
|
3375
|
+
if (!footerThemeBtn) return;
|
|
3376
|
+
const label = piCurrentTheme && piCurrentTheme.trim() ? piCurrentTheme.trim() : "theme";
|
|
3377
|
+
footerThemeBtn.textContent = "Theme: " + label;
|
|
3378
|
+
footerThemeBtn.title = "Switch the active Pi theme. Current theme: " + label + ". This affects terminal Pi and Studio.";
|
|
3379
|
+
footerThemeBtn.setAttribute("aria-expanded", footerThemeMenuOpen ? "true" : "false");
|
|
3380
|
+
}
|
|
3381
|
+
|
|
3382
|
+
function renderFooterThemeMenu() {
|
|
3383
|
+
if (!footerThemeMenuEl) return;
|
|
3384
|
+
const current = piCurrentTheme && piCurrentTheme.trim() ? piCurrentTheme.trim() : "";
|
|
3385
|
+
const optionNames = new Set(piThemeOptions.map((option) => option.name));
|
|
3386
|
+
const options = piThemeOptions.slice();
|
|
3387
|
+
if (current && !optionNames.has(current)) {
|
|
3388
|
+
options.unshift({ name: current, path: "" });
|
|
3389
|
+
}
|
|
3390
|
+
const optionsHtml = options.map((option) => {
|
|
3391
|
+
const selected = option.name === current ? " selected" : "";
|
|
3392
|
+
const title = option.path ? " title='" + escapeHtml(option.path) + "'" : "";
|
|
3393
|
+
return "<option value='" + escapeHtml(option.name) + "'" + selected + title + ">" + escapeHtml(option.name) + "</option>";
|
|
3394
|
+
});
|
|
3395
|
+
footerThemeMenuEl.innerHTML = ""
|
|
3396
|
+
+ "<div class='footer-model-menu-heading'>Pi theme</div>"
|
|
3397
|
+
+ (optionsHtml.length
|
|
3398
|
+
? "<label class='footer-model-menu-field'><span>Active theme</span><select id='footerPiThemeSelect'>" + optionsHtml.join("") + "</select></label>"
|
|
3399
|
+
: "<div class='footer-model-menu-note'>No Pi themes are available yet.</div>")
|
|
3400
|
+
+ "<div class='footer-model-menu-note'>Switches the active Pi theme and persists it to Pi settings.</div>";
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
function setFooterThemeMenuOpen(open) {
|
|
3404
|
+
footerThemeMenuOpen = Boolean(open);
|
|
3405
|
+
if (footerThemeMenuOpen) setFooterModelMenuOpen(false);
|
|
3406
|
+
if (footerThemeMenuEl) footerThemeMenuEl.hidden = !footerThemeMenuOpen;
|
|
3407
|
+
if (footerThemeBtn) {
|
|
3408
|
+
footerThemeBtn.classList.toggle("is-open", footerThemeMenuOpen);
|
|
3409
|
+
footerThemeBtn.setAttribute("aria-expanded", footerThemeMenuOpen ? "true" : "false");
|
|
3410
|
+
}
|
|
3411
|
+
if (footerThemeMenuOpen) renderFooterThemeMenu();
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
function requestPiThemeSelection(themeName) {
|
|
3415
|
+
const normalized = String(themeName || "").trim();
|
|
3416
|
+
if (!normalized) {
|
|
3417
|
+
setStatus("No Pi theme selected.", "warning");
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
3420
|
+
const sent = sendMessage({ type: "pi_theme_select_request", theme: normalized });
|
|
3421
|
+
if (sent) setStatus("Switching Pi theme…", "warning");
|
|
3422
|
+
}
|
|
3423
|
+
|
|
3301
3424
|
function updateFooterMeta() {
|
|
3302
3425
|
const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
|
|
3303
3426
|
const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
|
|
@@ -3795,33 +3918,33 @@
|
|
|
3795
3918
|
setStatus("Right pane content focused.");
|
|
3796
3919
|
}
|
|
3797
3920
|
|
|
3798
|
-
function
|
|
3799
|
-
const
|
|
3921
|
+
function switchRightPaneToView(nextView, options) {
|
|
3922
|
+
const requestedView = canonicalRightViewValue(nextView);
|
|
3923
|
+
const label = getRightViewDisplayLabel(requestedView);
|
|
3924
|
+
if (!isRightViewAvailableInCurrentMode(requestedView)) {
|
|
3925
|
+
setStatus(label + " is unavailable in editor-only Studio views.", "warning");
|
|
3926
|
+
return false;
|
|
3927
|
+
}
|
|
3800
3928
|
const snapshot = snapshotStudioScrollablePositions();
|
|
3801
|
-
setRightView(
|
|
3929
|
+
setRightView(requestedView);
|
|
3802
3930
|
scheduleStudioScrollablePositionRestore(snapshot);
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3931
|
+
if (!options || options.announce !== false) {
|
|
3932
|
+
setStatus("Right pane view: " + label + ".");
|
|
3933
|
+
}
|
|
3934
|
+
return true;
|
|
3935
|
+
}
|
|
3936
|
+
|
|
3937
|
+
function switchRightPaneToPrimaryPreview() {
|
|
3938
|
+
const targetView = isEditorOnlyMode ? "editor-preview" : "preview";
|
|
3939
|
+
switchRightPaneToView(targetView);
|
|
3807
3940
|
}
|
|
3808
3941
|
|
|
3809
3942
|
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.");
|
|
3943
|
+
switchRightPaneToView("trace");
|
|
3818
3944
|
}
|
|
3819
3945
|
|
|
3820
3946
|
function switchRightPaneToEditorPreview() {
|
|
3821
|
-
|
|
3822
|
-
setRightView("editor-preview");
|
|
3823
|
-
scheduleStudioScrollablePositionRestore(snapshot);
|
|
3824
|
-
setStatus("Right pane view: Editor (Preview).");
|
|
3947
|
+
switchRightPaneToView("editor-preview");
|
|
3825
3948
|
}
|
|
3826
3949
|
|
|
3827
3950
|
function cycleActivePaneView(direction) {
|
|
@@ -4122,6 +4245,17 @@
|
|
|
4122
4245
|
return;
|
|
4123
4246
|
}
|
|
4124
4247
|
|
|
4248
|
+
const numericRightViewShortcut = (event.metaKey || event.ctrlKey)
|
|
4249
|
+
&& event.altKey
|
|
4250
|
+
&& !event.shiftKey
|
|
4251
|
+
? (RIGHT_VIEW_NUMERIC_SHORTCUTS[code] || RIGHT_VIEW_NUMERIC_SHORTCUTS["Digit" + key])
|
|
4252
|
+
: null;
|
|
4253
|
+
if (numericRightViewShortcut) {
|
|
4254
|
+
event.preventDefault();
|
|
4255
|
+
switchRightPaneToView(numericRightViewShortcut);
|
|
4256
|
+
return;
|
|
4257
|
+
}
|
|
4258
|
+
|
|
4125
4259
|
const isPreviewShortcut = (key.toLowerCase() === "p" || code === "KeyP")
|
|
4126
4260
|
&& (event.metaKey || event.ctrlKey)
|
|
4127
4261
|
&& event.altKey
|
|
@@ -18827,6 +18961,7 @@
|
|
|
18827
18961
|
updateCompletionSuggestionModelOptions(message.suggestionModels);
|
|
18828
18962
|
}
|
|
18829
18963
|
updatePiSessionModelState(message);
|
|
18964
|
+
updatePiThemeState(message);
|
|
18830
18965
|
if (typeof message.terminalSessionLabel === "string") {
|
|
18831
18966
|
terminalSessionLabel = message.terminalSessionLabel;
|
|
18832
18967
|
}
|
|
@@ -19403,6 +19538,7 @@
|
|
|
19403
19538
|
updateCompletionSuggestionModelOptions(message.suggestionModels);
|
|
19404
19539
|
}
|
|
19405
19540
|
updatePiSessionModelState(message);
|
|
19541
|
+
updatePiThemeState(message);
|
|
19406
19542
|
if (typeof message.terminalSessionLabel === "string") {
|
|
19407
19543
|
terminalSessionLabel = message.terminalSessionLabel;
|
|
19408
19544
|
}
|
|
@@ -19536,6 +19672,7 @@
|
|
|
19536
19672
|
root.style.setProperty(key, message.vars[key]);
|
|
19537
19673
|
}
|
|
19538
19674
|
});
|
|
19675
|
+
updatePiThemeState(message);
|
|
19539
19676
|
updateDocumentTitle();
|
|
19540
19677
|
}
|
|
19541
19678
|
}
|
|
@@ -20283,6 +20420,7 @@
|
|
|
20283
20420
|
closeExportPreviewMenu();
|
|
20284
20421
|
closePreviewLinkMenu();
|
|
20285
20422
|
setFooterModelMenuOpen(false);
|
|
20423
|
+
setFooterThemeMenuOpen(false);
|
|
20286
20424
|
}
|
|
20287
20425
|
});
|
|
20288
20426
|
|
|
@@ -20309,11 +20447,36 @@
|
|
|
20309
20447
|
}
|
|
20310
20448
|
});
|
|
20311
20449
|
}
|
|
20450
|
+
if (footerThemeBtn) {
|
|
20451
|
+
footerThemeBtn.addEventListener("click", (event) => {
|
|
20452
|
+
event.preventDefault();
|
|
20453
|
+
event.stopPropagation();
|
|
20454
|
+
setFooterThemeMenuOpen(!footerThemeMenuOpen);
|
|
20455
|
+
});
|
|
20456
|
+
}
|
|
20457
|
+
if (footerThemeMenuEl) {
|
|
20458
|
+
footerThemeMenuEl.addEventListener("click", (event) => {
|
|
20459
|
+
event.stopPropagation();
|
|
20460
|
+
});
|
|
20461
|
+
footerThemeMenuEl.addEventListener("change", (event) => {
|
|
20462
|
+
const target = event.target;
|
|
20463
|
+
if (!(target instanceof HTMLSelectElement)) return;
|
|
20464
|
+
if (target.id === "footerPiThemeSelect") {
|
|
20465
|
+
requestPiThemeSelection(target.value);
|
|
20466
|
+
setFooterThemeMenuOpen(false);
|
|
20467
|
+
}
|
|
20468
|
+
});
|
|
20469
|
+
}
|
|
20312
20470
|
document.addEventListener("click", (event) => {
|
|
20313
20471
|
const target = event.target;
|
|
20314
20472
|
if (target instanceof Element && (target.closest("#footerModelMenu") || target.closest("#footerMetaModel"))) return;
|
|
20315
20473
|
setFooterModelMenuOpen(false);
|
|
20316
20474
|
});
|
|
20475
|
+
document.addEventListener("click", (event) => {
|
|
20476
|
+
const target = event.target;
|
|
20477
|
+
if (target instanceof Element && (target.closest("#footerThemeMenu") || target.closest("#footerThemeBtn"))) return;
|
|
20478
|
+
setFooterThemeMenuOpen(false);
|
|
20479
|
+
});
|
|
20317
20480
|
|
|
20318
20481
|
saveAsBtn.addEventListener("click", () => {
|
|
20319
20482
|
const content = sourceTextEl.value;
|
package/client/studio.css
CHANGED
|
@@ -4277,11 +4277,10 @@
|
|
|
4277
4277
|
outline: none;
|
|
4278
4278
|
}
|
|
4279
4279
|
|
|
4280
|
-
.footer-model-menu
|
|
4280
|
+
.footer-model-menu,
|
|
4281
|
+
.footer-theme-menu {
|
|
4281
4282
|
position: fixed;
|
|
4282
|
-
left: 12px;
|
|
4283
4283
|
bottom: 38px;
|
|
4284
|
-
width: min(440px, calc(100vw - 24px));
|
|
4285
4284
|
padding: 10px;
|
|
4286
4285
|
border: 1px solid var(--panel-border);
|
|
4287
4286
|
border-radius: 12px;
|
|
@@ -4291,7 +4290,18 @@
|
|
|
4291
4290
|
z-index: 110;
|
|
4292
4291
|
}
|
|
4293
4292
|
|
|
4294
|
-
.footer-model-menu
|
|
4293
|
+
.footer-model-menu {
|
|
4294
|
+
left: 12px;
|
|
4295
|
+
width: min(440px, calc(100vw - 24px));
|
|
4296
|
+
}
|
|
4297
|
+
|
|
4298
|
+
.footer-theme-menu {
|
|
4299
|
+
right: 12px;
|
|
4300
|
+
width: min(320px, calc(100vw - 24px));
|
|
4301
|
+
}
|
|
4302
|
+
|
|
4303
|
+
.footer-model-menu[hidden],
|
|
4304
|
+
.footer-theme-menu[hidden] {
|
|
4295
4305
|
display: none !important;
|
|
4296
4306
|
}
|
|
4297
4307
|
|
|
@@ -4348,10 +4358,18 @@
|
|
|
4348
4358
|
opacity: 0.72;
|
|
4349
4359
|
}
|
|
4350
4360
|
|
|
4351
|
-
.
|
|
4361
|
+
.footer-actions {
|
|
4352
4362
|
grid-area: hint;
|
|
4353
4363
|
justify-self: end;
|
|
4354
4364
|
align-self: center;
|
|
4365
|
+
display: inline-flex;
|
|
4366
|
+
align-items: center;
|
|
4367
|
+
justify-content: flex-end;
|
|
4368
|
+
gap: 6px;
|
|
4369
|
+
min-width: 0;
|
|
4370
|
+
}
|
|
4371
|
+
|
|
4372
|
+
.shortcut-hint {
|
|
4355
4373
|
display: inline-flex;
|
|
4356
4374
|
align-items: center;
|
|
4357
4375
|
gap: 6px;
|
|
@@ -4366,12 +4384,21 @@
|
|
|
4366
4384
|
opacity: 0.9;
|
|
4367
4385
|
}
|
|
4368
4386
|
|
|
4387
|
+
.footer-theme-btn {
|
|
4388
|
+
max-width: min(28ch, 36vw);
|
|
4389
|
+
min-width: 0;
|
|
4390
|
+
overflow: hidden;
|
|
4391
|
+
text-overflow: ellipsis;
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4369
4394
|
.shortcut-hint:not(:disabled):hover,
|
|
4370
|
-
.shortcut-hint:focus-visible
|
|
4395
|
+
.shortcut-hint:focus-visible,
|
|
4396
|
+
.shortcut-hint.is-open {
|
|
4371
4397
|
background: var(--panel-2);
|
|
4372
4398
|
border-color: var(--control-border);
|
|
4373
4399
|
color: var(--text);
|
|
4374
4400
|
opacity: 1;
|
|
4401
|
+
outline: none;
|
|
4375
4402
|
}
|
|
4376
4403
|
|
|
4377
4404
|
.footer-compact-btn {
|
|
@@ -4398,7 +4425,7 @@
|
|
|
4398
4425
|
}
|
|
4399
4426
|
|
|
4400
4427
|
body.studio-zen-mode .footer-meta,
|
|
4401
|
-
body.studio-zen-mode .
|
|
4428
|
+
body.studio-zen-mode .footer-actions {
|
|
4402
4429
|
display: none !important;
|
|
4403
4430
|
}
|
|
4404
4431
|
|
|
@@ -4993,8 +5020,12 @@
|
|
|
4993
5020
|
max-width: 100%;
|
|
4994
5021
|
}
|
|
4995
5022
|
|
|
4996
|
-
.
|
|
5023
|
+
.footer-actions {
|
|
4997
5024
|
justify-self: start;
|
|
5025
|
+
flex-wrap: wrap;
|
|
5026
|
+
}
|
|
5027
|
+
|
|
5028
|
+
.shortcut-hint {
|
|
4998
5029
|
text-align: left;
|
|
4999
5030
|
white-space: normal;
|
|
5000
5031
|
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
|
|
@@ -8425,6 +8431,16 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
8425
8431
|
}
|
|
8426
8432
|
}
|
|
8427
8433
|
|
|
8434
|
+
if (msg.type === "pi_theme_select_request" && typeof msg.theme === "string") {
|
|
8435
|
+
const theme = msg.theme.trim();
|
|
8436
|
+
if (theme && theme.length <= 200) {
|
|
8437
|
+
return {
|
|
8438
|
+
type: "pi_theme_select_request",
|
|
8439
|
+
theme,
|
|
8440
|
+
};
|
|
8441
|
+
}
|
|
8442
|
+
}
|
|
8443
|
+
|
|
8428
8444
|
if (msg.type === "completion_suggestion_request" && typeof msg.requestId === "string" && typeof msg.text === "string") {
|
|
8429
8445
|
const textLength = msg.text.length;
|
|
8430
8446
|
const rawStart = typeof msg.selectionStart === "number" && Number.isFinite(msg.selectionStart) ? msg.selectionStart : textLength;
|
|
@@ -10277,6 +10293,7 @@ function buildStudioHtml(
|
|
|
10277
10293
|
const initialModel = escapeHtmlForInline(initialModelLabel ?? "none");
|
|
10278
10294
|
const initialTerminal = escapeHtmlForInline(initialTerminalLabel ?? "unknown");
|
|
10279
10295
|
const initialTerminalDetailAttr = escapeHtmlForInline(initialTerminalDetail ?? initialTerminalLabel ?? "unknown");
|
|
10296
|
+
const initialTheme = escapeHtmlForInline(theme?.name ?? "theme");
|
|
10280
10297
|
const initialContextTokens =
|
|
10281
10298
|
typeof initialContextUsage?.tokens === "number" && Number.isFinite(initialContextUsage.tokens)
|
|
10282
10299
|
? String(initialContextUsage.tokens)
|
|
@@ -10344,7 +10361,7 @@ ${cssVarsBlock}
|
|
|
10344
10361
|
</style>
|
|
10345
10362
|
<link rel="stylesheet" href="${stylesheetHref}" />
|
|
10346
10363
|
</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}">
|
|
10364
|
+
<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
10365
|
<header>
|
|
10349
10366
|
<h1><span class="app-logo" aria-hidden="true">π</span> Studio <span class="app-subtitle">${appSubtitle}</span></h1>
|
|
10350
10367
|
<div class="controls">
|
|
@@ -10561,7 +10578,7 @@ ${cssVarsBlock}
|
|
|
10561
10578
|
<section id="rightPane">
|
|
10562
10579
|
<div id="rightSectionHeader" class="section-header">
|
|
10563
10580
|
<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+
|
|
10581
|
+
<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
10582
|
<option value="markdown">Response (Raw)</option>
|
|
10566
10583
|
<option value="preview" selected>Response (Preview)</option>
|
|
10567
10584
|
<option value="editor-preview">Editor (Preview)</option>
|
|
@@ -10637,7 +10654,11 @@ ${cssVarsBlock}
|
|
|
10637
10654
|
<span id="statusLine"><span id="statusSpinner" aria-hidden="true"> </span><span id="status">Booting studio…</span></span>
|
|
10638
10655
|
<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
10656
|
<div id="footerModelMenu" class="footer-model-menu" hidden></div>
|
|
10640
|
-
<
|
|
10657
|
+
<div id="footerThemeMenu" class="footer-theme-menu" hidden></div>
|
|
10658
|
+
<span class="footer-actions">
|
|
10659
|
+
<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>
|
|
10660
|
+
<button id="shortcutsBtn" class="shortcut-hint" type="button" title="Show Studio keyboard shortcuts. Press ? when not editing text.">Shortcuts (?)</button>
|
|
10661
|
+
</span>
|
|
10641
10662
|
</footer>
|
|
10642
10663
|
|
|
10643
10664
|
<div id="shortcutsOverlay" class="shortcuts-overlay" hidden>
|
|
@@ -10655,7 +10676,8 @@ ${cssVarsBlock}
|
|
|
10655
10676
|
<dl>
|
|
10656
10677
|
<div><dt>F6</dt><dd>Switch between editor and right pane</dd></div>
|
|
10657
10678
|
<div><dt>F7 / Shift+F7</dt><dd>Cycle the active pane's view</dd></div>
|
|
10658
|
-
<div><dt>Cmd/Ctrl+Alt+
|
|
10679
|
+
<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>
|
|
10680
|
+
<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
10681
|
<div><dt>Cmd/Ctrl+Alt+E</dt><dd>Switch the right pane directly to Editor Preview</dd></div>
|
|
10660
10682
|
<div><dt>Cmd/Ctrl+Alt+W</dt><dd>Switch the right pane directly to Working</dd></div>
|
|
10661
10683
|
<div><dt>F8</dt><dd>Focus editor text</dd></div>
|
|
@@ -11770,6 +11792,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
11770
11792
|
}
|
|
11771
11793
|
: null;
|
|
11772
11794
|
|
|
11795
|
+
const getStudioThemeOptions = () => {
|
|
11796
|
+
try {
|
|
11797
|
+
const themes = lastCommandCtx?.ui?.getAllThemes?.() ?? [];
|
|
11798
|
+
return themes
|
|
11799
|
+
.map((theme) => ({
|
|
11800
|
+
name: typeof theme.name === "string" ? theme.name.trim() : "",
|
|
11801
|
+
path: typeof theme.path === "string" ? theme.path : undefined,
|
|
11802
|
+
}))
|
|
11803
|
+
.filter((theme) => theme.name);
|
|
11804
|
+
} catch {
|
|
11805
|
+
return [];
|
|
11806
|
+
}
|
|
11807
|
+
};
|
|
11808
|
+
|
|
11809
|
+
const getCurrentStudioThemeDescriptor = () => {
|
|
11810
|
+
const name = typeof lastCommandCtx?.ui?.theme?.name === "string" ? lastCommandCtx.ui.theme.name.trim() : "";
|
|
11811
|
+
return name ? { name } : null;
|
|
11812
|
+
};
|
|
11813
|
+
|
|
11773
11814
|
const broadcastState = () => {
|
|
11774
11815
|
terminalSessionLabel = buildTerminalSessionLabel(studioCwd, getSessionNameSafe());
|
|
11775
11816
|
terminalSessionDetail = buildTerminalSessionDetail(studioCwd, getSessionNameSafe());
|
|
@@ -11788,6 +11829,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
11788
11829
|
thinkingLevel: getThinkingLevelSafe() ?? "off",
|
|
11789
11830
|
piModels: modelOptions,
|
|
11790
11831
|
suggestionModels: modelOptions,
|
|
11832
|
+
currentTheme: getCurrentStudioThemeDescriptor(),
|
|
11833
|
+
piThemes: getStudioThemeOptions(),
|
|
11791
11834
|
terminalSessionLabel,
|
|
11792
11835
|
terminalSessionDetail,
|
|
11793
11836
|
contextTokens: contextUsageSnapshot.tokens,
|
|
@@ -12087,6 +12130,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
12087
12130
|
thinkingLevel: getThinkingLevelSafe() ?? "off",
|
|
12088
12131
|
piModels: getStudioModelOptions(),
|
|
12089
12132
|
suggestionModels: getStudioModelOptions(),
|
|
12133
|
+
currentTheme: getCurrentStudioThemeDescriptor(),
|
|
12134
|
+
piThemes: getStudioThemeOptions(),
|
|
12090
12135
|
terminalSessionLabel,
|
|
12091
12136
|
terminalSessionDetail,
|
|
12092
12137
|
contextTokens: contextUsageSnapshot.tokens,
|
|
@@ -12146,6 +12191,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
12146
12191
|
return;
|
|
12147
12192
|
}
|
|
12148
12193
|
|
|
12194
|
+
if (msg.type === "pi_theme_select_request") {
|
|
12195
|
+
const ui = lastCommandCtx?.ui;
|
|
12196
|
+
if (!ui || typeof ui.setTheme !== "function") {
|
|
12197
|
+
sendToClient(client, { type: "info", level: "warning", message: "Pi theme controls are not available yet." });
|
|
12198
|
+
return;
|
|
12199
|
+
}
|
|
12200
|
+
const themeName = msg.theme.trim();
|
|
12201
|
+
const availableThemes = getStudioThemeOptions();
|
|
12202
|
+
if (availableThemes.length && !availableThemes.some((theme) => theme.name === themeName)) {
|
|
12203
|
+
sendToClient(client, { type: "info", level: "warning", message: `Pi theme not found: ${themeName}` });
|
|
12204
|
+
return;
|
|
12205
|
+
}
|
|
12206
|
+
try {
|
|
12207
|
+
const result = ui.setTheme(themeName);
|
|
12208
|
+
if (!result.success) {
|
|
12209
|
+
sendToClient(client, { type: "info", level: "error", message: `Theme switch failed: ${result.error ?? themeName}` });
|
|
12210
|
+
return;
|
|
12211
|
+
}
|
|
12212
|
+
const vars = buildThemeCssVars(getStudioThemeStyle(ui.theme));
|
|
12213
|
+
lastThemeVarsJson = JSON.stringify(vars);
|
|
12214
|
+
syncCmuxStudioStatus();
|
|
12215
|
+
broadcast({ type: "theme_update", vars, currentTheme: getCurrentStudioThemeDescriptor(), piThemes: getStudioThemeOptions() });
|
|
12216
|
+
broadcastState();
|
|
12217
|
+
sendToClient(client, { type: "info", level: "info", message: `Pi theme switched to ${themeName}.` });
|
|
12218
|
+
} catch (error) {
|
|
12219
|
+
sendToClient(client, { type: "info", level: "error", message: `Theme switch failed: ${error instanceof Error ? error.message : String(error)}` });
|
|
12220
|
+
}
|
|
12221
|
+
return;
|
|
12222
|
+
}
|
|
12223
|
+
|
|
12149
12224
|
if (msg.type === "get_latest_response") {
|
|
12150
12225
|
if (!lastStudioResponse) {
|
|
12151
12226
|
sendToClient(client, { type: "info", message: "No latest assistant response is available yet." });
|
|
@@ -14335,7 +14410,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14335
14410
|
lastThemeVarsJson = json;
|
|
14336
14411
|
syncCmuxStudioStatus();
|
|
14337
14412
|
for (const client of serverState.clients) {
|
|
14338
|
-
sendToClient(client, { type: "theme_update", vars });
|
|
14413
|
+
sendToClient(client, { type: "theme_update", vars, currentTheme: getCurrentStudioThemeDescriptor(), piThemes: getStudioThemeOptions() });
|
|
14339
14414
|
}
|
|
14340
14415
|
}
|
|
14341
14416
|
} catch {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.34",
|
|
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",
|