pi-studio 0.9.32 → 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 +21 -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 +203 -23
- package/client/studio.css +83 -36
- package/index.ts +141 -15
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ 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
|
+
|
|
14
|
+
## [0.9.33] — 2026-06-16
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Added `Cmd/Ctrl+Alt+E` to switch the right pane directly to Editor Preview.
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Calmed annotation and comment affordance styling so annotations read more like highlighted feedback and less like heavy badges.
|
|
21
|
+
- Slightly muted active Stop button styling while keeping it in the theme error colour family.
|
|
22
|
+
- Derived PDF annotation colours from the active theme accent hue while keeping a light, print-friendly background.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Preserved the Working view scroll position when switching between `All`, `Thinking`, and `Tools` filters unless the view was already pinned to the bottom.
|
|
26
|
+
- Rendered standalone long `[an: ...]` annotations correctly in LaTeX-mode PDF exports even when Pandoc wraps the escaped marker across generated LaTeX lines.
|
|
27
|
+
|
|
7
28
|
## [0.9.32] — 2026-06-10
|
|
8
29
|
|
|
9
30
|
### 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+
|
|
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;
|
|
@@ -859,7 +907,7 @@
|
|
|
859
907
|
const normalized = normalizeTraceFilter(nextFilter);
|
|
860
908
|
if (traceFilter === normalized) return;
|
|
861
909
|
traceFilter = normalized;
|
|
862
|
-
traceAutoScroll =
|
|
910
|
+
traceAutoScroll = rightView === "trace" ? shouldStickTraceToBottom() : traceAutoScroll;
|
|
863
911
|
renderTraceViewIfActive();
|
|
864
912
|
}
|
|
865
913
|
|
|
@@ -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,26 +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
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
setRightView("trace");
|
|
3816
|
-
scheduleStudioScrollablePositionRestore(snapshot);
|
|
3817
|
-
setStatus("Right pane view: Working.");
|
|
3943
|
+
switchRightPaneToView("trace");
|
|
3944
|
+
}
|
|
3945
|
+
|
|
3946
|
+
function switchRightPaneToEditorPreview() {
|
|
3947
|
+
switchRightPaneToView("editor-preview");
|
|
3818
3948
|
}
|
|
3819
3949
|
|
|
3820
3950
|
function cycleActivePaneView(direction) {
|
|
@@ -4115,6 +4245,17 @@
|
|
|
4115
4245
|
return;
|
|
4116
4246
|
}
|
|
4117
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
|
+
|
|
4118
4259
|
const isPreviewShortcut = (key.toLowerCase() === "p" || code === "KeyP")
|
|
4119
4260
|
&& (event.metaKey || event.ctrlKey)
|
|
4120
4261
|
&& event.altKey
|
|
@@ -4135,6 +4276,16 @@
|
|
|
4135
4276
|
return;
|
|
4136
4277
|
}
|
|
4137
4278
|
|
|
4279
|
+
const isEditorPreviewShortcut = (key.toLowerCase() === "e" || code === "KeyE")
|
|
4280
|
+
&& (event.metaKey || event.ctrlKey)
|
|
4281
|
+
&& event.altKey
|
|
4282
|
+
&& !event.shiftKey;
|
|
4283
|
+
if (isEditorPreviewShortcut) {
|
|
4284
|
+
event.preventDefault();
|
|
4285
|
+
switchRightPaneToEditorPreview();
|
|
4286
|
+
return;
|
|
4287
|
+
}
|
|
4288
|
+
|
|
4138
4289
|
const isContentFocusShortcut = key === "F8" && !event.metaKey && !event.ctrlKey && !event.altKey;
|
|
4139
4290
|
if (isContentFocusShortcut) {
|
|
4140
4291
|
event.preventDefault();
|
|
@@ -18810,6 +18961,7 @@
|
|
|
18810
18961
|
updateCompletionSuggestionModelOptions(message.suggestionModels);
|
|
18811
18962
|
}
|
|
18812
18963
|
updatePiSessionModelState(message);
|
|
18964
|
+
updatePiThemeState(message);
|
|
18813
18965
|
if (typeof message.terminalSessionLabel === "string") {
|
|
18814
18966
|
terminalSessionLabel = message.terminalSessionLabel;
|
|
18815
18967
|
}
|
|
@@ -19386,6 +19538,7 @@
|
|
|
19386
19538
|
updateCompletionSuggestionModelOptions(message.suggestionModels);
|
|
19387
19539
|
}
|
|
19388
19540
|
updatePiSessionModelState(message);
|
|
19541
|
+
updatePiThemeState(message);
|
|
19389
19542
|
if (typeof message.terminalSessionLabel === "string") {
|
|
19390
19543
|
terminalSessionLabel = message.terminalSessionLabel;
|
|
19391
19544
|
}
|
|
@@ -19519,6 +19672,7 @@
|
|
|
19519
19672
|
root.style.setProperty(key, message.vars[key]);
|
|
19520
19673
|
}
|
|
19521
19674
|
});
|
|
19675
|
+
updatePiThemeState(message);
|
|
19522
19676
|
updateDocumentTitle();
|
|
19523
19677
|
}
|
|
19524
19678
|
}
|
|
@@ -20266,6 +20420,7 @@
|
|
|
20266
20420
|
closeExportPreviewMenu();
|
|
20267
20421
|
closePreviewLinkMenu();
|
|
20268
20422
|
setFooterModelMenuOpen(false);
|
|
20423
|
+
setFooterThemeMenuOpen(false);
|
|
20269
20424
|
}
|
|
20270
20425
|
});
|
|
20271
20426
|
|
|
@@ -20292,11 +20447,36 @@
|
|
|
20292
20447
|
}
|
|
20293
20448
|
});
|
|
20294
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
|
+
}
|
|
20295
20470
|
document.addEventListener("click", (event) => {
|
|
20296
20471
|
const target = event.target;
|
|
20297
20472
|
if (target instanceof Element && (target.closest("#footerModelMenu") || target.closest("#footerMetaModel"))) return;
|
|
20298
20473
|
setFooterModelMenuOpen(false);
|
|
20299
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
|
+
});
|
|
20300
20480
|
|
|
20301
20481
|
saveAsBtn.addEventListener("click", () => {
|
|
20302
20482
|
const content = sourceTextEl.value;
|
package/client/studio.css
CHANGED
|
@@ -210,15 +210,17 @@
|
|
|
210
210
|
|
|
211
211
|
#sendRunBtn.request-stop-active,
|
|
212
212
|
#critiqueBtn.request-stop-active {
|
|
213
|
-
background: var(--error);
|
|
214
|
-
border-color: var(--error);
|
|
213
|
+
background: color-mix(in srgb, var(--error) 82%, var(--panel));
|
|
214
|
+
border-color: color-mix(in srgb, var(--error) 88%, var(--panel));
|
|
215
215
|
color: var(--error-contrast);
|
|
216
|
-
font-weight:
|
|
216
|
+
font-weight: 700;
|
|
217
217
|
}
|
|
218
218
|
|
|
219
219
|
#sendRunBtn.request-stop-active:not(:disabled):hover,
|
|
220
220
|
#critiqueBtn.request-stop-active:not(:disabled):hover {
|
|
221
|
-
|
|
221
|
+
background: var(--error);
|
|
222
|
+
border-color: var(--error);
|
|
223
|
+
filter: none;
|
|
222
224
|
}
|
|
223
225
|
|
|
224
226
|
.file-label {
|
|
@@ -930,17 +932,17 @@
|
|
|
930
932
|
padding: 0 10px;
|
|
931
933
|
font-size: 12px;
|
|
932
934
|
line-height: 1;
|
|
933
|
-
background: var(--accent-soft);
|
|
934
|
-
border-color: var(--
|
|
935
|
-
color: var(--
|
|
935
|
+
background: color-mix(in srgb, var(--panel) 88%, var(--accent-soft));
|
|
936
|
+
border-color: var(--control-border);
|
|
937
|
+
color: var(--text);
|
|
936
938
|
box-shadow: 0 1px 2px var(--shadow-color);
|
|
937
939
|
}
|
|
938
940
|
|
|
939
941
|
.editor-selection-action-btn:hover,
|
|
940
942
|
.editor-selection-action-btn:focus-visible {
|
|
941
|
-
background: var(--accent-soft
|
|
943
|
+
background: var(--accent-soft);
|
|
942
944
|
color: var(--accent);
|
|
943
|
-
border-color: var(--accent);
|
|
945
|
+
border-color: color-mix(in srgb, var(--accent) 45%, var(--control-border));
|
|
944
946
|
}
|
|
945
947
|
|
|
946
948
|
.editor-selection-actions[hidden],
|
|
@@ -1043,12 +1045,12 @@
|
|
|
1043
1045
|
}
|
|
1044
1046
|
|
|
1045
1047
|
.hl-annotation {
|
|
1046
|
-
color: var(--accent);
|
|
1047
|
-
background: var(--accent-soft);
|
|
1048
|
+
color: color-mix(in srgb, var(--accent) 72%, var(--text));
|
|
1049
|
+
background: color-mix(in srgb, var(--accent-soft) 58%, transparent);
|
|
1048
1050
|
border: 0;
|
|
1049
|
-
border-radius:
|
|
1051
|
+
border-radius: 3px;
|
|
1050
1052
|
padding: 0;
|
|
1051
|
-
box-shadow: inset 0 0 0
|
|
1053
|
+
box-shadow: inset 0 -0.08em 0 color-mix(in srgb, var(--accent) 26%, transparent);
|
|
1052
1054
|
}
|
|
1053
1055
|
|
|
1054
1056
|
.hl-annotation-muted {
|
|
@@ -1057,10 +1059,10 @@
|
|
|
1057
1059
|
}
|
|
1058
1060
|
|
|
1059
1061
|
.annotation-preview-marker {
|
|
1060
|
-
color: var(--accent);
|
|
1061
|
-
background: var(--accent-soft);
|
|
1062
|
-
border: 1px solid var(--marker-border);
|
|
1063
|
-
border-radius:
|
|
1062
|
+
color: color-mix(in srgb, var(--accent) 72%, var(--text));
|
|
1063
|
+
background: color-mix(in srgb, var(--accent-soft) 58%, transparent);
|
|
1064
|
+
border: 1px solid color-mix(in srgb, var(--marker-border) 26%, transparent);
|
|
1065
|
+
border-radius: 3px;
|
|
1064
1066
|
display: inline-block;
|
|
1065
1067
|
max-width: 100%;
|
|
1066
1068
|
padding: 0 4px;
|
|
@@ -1068,6 +1070,20 @@
|
|
|
1068
1070
|
vertical-align: baseline;
|
|
1069
1071
|
}
|
|
1070
1072
|
|
|
1073
|
+
.annotation-preview-marker strong {
|
|
1074
|
+
color: color-mix(in srgb, var(--accent) 82%, var(--text));
|
|
1075
|
+
font-weight: 750;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
.annotation-preview-marker em {
|
|
1079
|
+
color: color-mix(in srgb, currentColor 82%, var(--muted));
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
.annotation-preview-marker s {
|
|
1083
|
+
color: color-mix(in srgb, currentColor 70%, var(--muted));
|
|
1084
|
+
text-decoration-thickness: 0.08em;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1071
1087
|
.annotation-preview-marker code {
|
|
1072
1088
|
font-family: var(--mono-font, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
1073
1089
|
font-size: 0.95em;
|
|
@@ -1216,9 +1232,9 @@
|
|
|
1216
1232
|
padding: 0 10px;
|
|
1217
1233
|
font-size: 12px;
|
|
1218
1234
|
line-height: 1;
|
|
1219
|
-
background: var(--accent-soft);
|
|
1220
|
-
border-color: var(--
|
|
1221
|
-
color: var(--
|
|
1235
|
+
background: color-mix(in srgb, var(--panel) 88%, var(--accent-soft));
|
|
1236
|
+
border-color: var(--control-border);
|
|
1237
|
+
color: var(--text);
|
|
1222
1238
|
box-shadow: 0 1px 2px var(--shadow-color);
|
|
1223
1239
|
pointer-events: auto;
|
|
1224
1240
|
}
|
|
@@ -1227,9 +1243,9 @@
|
|
|
1227
1243
|
.preview-comment-add:focus-visible,
|
|
1228
1244
|
.preview-comment-jump:hover,
|
|
1229
1245
|
.preview-comment-jump:focus-visible {
|
|
1230
|
-
background: var(--accent-soft
|
|
1246
|
+
background: var(--accent-soft);
|
|
1231
1247
|
color: var(--accent);
|
|
1232
|
-
border-color: var(--accent);
|
|
1248
|
+
border-color: color-mix(in srgb, var(--accent) 45%, var(--control-border));
|
|
1233
1249
|
}
|
|
1234
1250
|
|
|
1235
1251
|
.preview-jump-highlight {
|
|
@@ -4261,11 +4277,10 @@
|
|
|
4261
4277
|
outline: none;
|
|
4262
4278
|
}
|
|
4263
4279
|
|
|
4264
|
-
.footer-model-menu
|
|
4280
|
+
.footer-model-menu,
|
|
4281
|
+
.footer-theme-menu {
|
|
4265
4282
|
position: fixed;
|
|
4266
|
-
left: 12px;
|
|
4267
4283
|
bottom: 38px;
|
|
4268
|
-
width: min(440px, calc(100vw - 24px));
|
|
4269
4284
|
padding: 10px;
|
|
4270
4285
|
border: 1px solid var(--panel-border);
|
|
4271
4286
|
border-radius: 12px;
|
|
@@ -4275,7 +4290,18 @@
|
|
|
4275
4290
|
z-index: 110;
|
|
4276
4291
|
}
|
|
4277
4292
|
|
|
4278
|
-
.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] {
|
|
4279
4305
|
display: none !important;
|
|
4280
4306
|
}
|
|
4281
4307
|
|
|
@@ -4332,10 +4358,18 @@
|
|
|
4332
4358
|
opacity: 0.72;
|
|
4333
4359
|
}
|
|
4334
4360
|
|
|
4335
|
-
.
|
|
4361
|
+
.footer-actions {
|
|
4336
4362
|
grid-area: hint;
|
|
4337
4363
|
justify-self: end;
|
|
4338
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 {
|
|
4339
4373
|
display: inline-flex;
|
|
4340
4374
|
align-items: center;
|
|
4341
4375
|
gap: 6px;
|
|
@@ -4350,12 +4384,21 @@
|
|
|
4350
4384
|
opacity: 0.9;
|
|
4351
4385
|
}
|
|
4352
4386
|
|
|
4387
|
+
.footer-theme-btn {
|
|
4388
|
+
max-width: min(28ch, 36vw);
|
|
4389
|
+
min-width: 0;
|
|
4390
|
+
overflow: hidden;
|
|
4391
|
+
text-overflow: ellipsis;
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4353
4394
|
.shortcut-hint:not(:disabled):hover,
|
|
4354
|
-
.shortcut-hint:focus-visible
|
|
4395
|
+
.shortcut-hint:focus-visible,
|
|
4396
|
+
.shortcut-hint.is-open {
|
|
4355
4397
|
background: var(--panel-2);
|
|
4356
4398
|
border-color: var(--control-border);
|
|
4357
4399
|
color: var(--text);
|
|
4358
4400
|
opacity: 1;
|
|
4401
|
+
outline: none;
|
|
4359
4402
|
}
|
|
4360
4403
|
|
|
4361
4404
|
.footer-compact-btn {
|
|
@@ -4382,7 +4425,7 @@
|
|
|
4382
4425
|
}
|
|
4383
4426
|
|
|
4384
4427
|
body.studio-zen-mode .footer-meta,
|
|
4385
|
-
body.studio-zen-mode .
|
|
4428
|
+
body.studio-zen-mode .footer-actions {
|
|
4386
4429
|
display: none !important;
|
|
4387
4430
|
}
|
|
4388
4431
|
|
|
@@ -4977,8 +5020,12 @@
|
|
|
4977
5020
|
max-width: 100%;
|
|
4978
5021
|
}
|
|
4979
5022
|
|
|
4980
|
-
.
|
|
5023
|
+
.footer-actions {
|
|
4981
5024
|
justify-self: start;
|
|
5025
|
+
flex-wrap: wrap;
|
|
5026
|
+
}
|
|
5027
|
+
|
|
5028
|
+
.shortcut-hint {
|
|
4982
5029
|
text-align: left;
|
|
4983
5030
|
white-space: normal;
|
|
4984
5031
|
flex-wrap: wrap;
|
|
@@ -5779,10 +5826,10 @@
|
|
|
5779
5826
|
}
|
|
5780
5827
|
|
|
5781
5828
|
body.studio-ui-refresh .studio-refresh-review-btn.request-stop-active {
|
|
5782
|
-
background: var(--error);
|
|
5783
|
-
border-color: var(--error);
|
|
5829
|
+
background: color-mix(in srgb, var(--error) 82%, var(--panel));
|
|
5830
|
+
border-color: color-mix(in srgb, var(--error) 88%, var(--panel));
|
|
5784
5831
|
color: var(--error-contrast);
|
|
5785
|
-
font-weight:
|
|
5832
|
+
font-weight: 700;
|
|
5786
5833
|
}
|
|
5787
5834
|
|
|
5788
5835
|
body.studio-ui-refresh .studio-refresh-review-btn.request-stop-active::after {
|
|
@@ -5790,10 +5837,10 @@
|
|
|
5790
5837
|
}
|
|
5791
5838
|
|
|
5792
5839
|
body.studio-ui-refresh .studio-refresh-menu #critiqueBtn.request-stop-active {
|
|
5793
|
-
background: var(--error);
|
|
5794
|
-
border-color: var(--error);
|
|
5840
|
+
background: color-mix(in srgb, var(--error) 82%, var(--panel));
|
|
5841
|
+
border-color: color-mix(in srgb, var(--error) 88%, var(--panel));
|
|
5795
5842
|
color: var(--error-contrast);
|
|
5796
|
-
font-weight:
|
|
5843
|
+
font-weight: 700;
|
|
5797
5844
|
}
|
|
5798
5845
|
|
|
5799
5846
|
.studio-preview-link-menu {
|
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
|
|
@@ -1066,7 +1072,23 @@ function buildStudioPdfCalloutTitleSizeCommand(options?: StudioPdfRenderOptions)
|
|
|
1066
1072
|
return "\\footnotesize";
|
|
1067
1073
|
}
|
|
1068
1074
|
|
|
1069
|
-
function
|
|
1075
|
+
function formatStudioPdfHtmlColor(color: string, fallback: string): string {
|
|
1076
|
+
return hexToRgb(color) ? color.replace(/^#/, "").toUpperCase() : fallback;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function buildStudioPdfAnnotationColors(style: StudioThemeStyle): { bg: string; border: string; text: string } {
|
|
1080
|
+
const accent = hexToRgb(style.palette.accent) ? style.palette.accent : "#2F6FEB";
|
|
1081
|
+
const page = "#FFFFFF";
|
|
1082
|
+
const darkText = "#0E1616";
|
|
1083
|
+
return {
|
|
1084
|
+
bg: formatStudioPdfHtmlColor(blendColors(accent, page, 0.91), "F1F7FF"),
|
|
1085
|
+
border: formatStudioPdfHtmlColor(blendColors(accent, page, 0.68), "C7DDFF"),
|
|
1086
|
+
text: formatStudioPdfHtmlColor(blendColors(accent, darkText, style.mode === "light" ? 0.40 : 0.34), "1F5FBF"),
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
function buildStudioPdfPreamble(options?: StudioPdfRenderOptions, extraPreamble = "", themeStyle?: StudioThemeStyle): string {
|
|
1091
|
+
const annotationColors = buildStudioPdfAnnotationColors(themeStyle ?? getStudioThemeStyle());
|
|
1070
1092
|
const sectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.sectionSize, "\\Large");
|
|
1071
1093
|
const subsectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.subsectionSize, "\\large");
|
|
1072
1094
|
const subsubsectionHeadingSize = buildStudioPdfHeadingSizeCommand(options?.subsubsectionSize, "\\normalsize");
|
|
@@ -1088,9 +1110,9 @@ function buildStudioPdfPreamble(options?: StudioPdfRenderOptions, extraPreamble
|
|
|
1088
1110
|
\\usepackage{xcolor}
|
|
1089
1111
|
\\usepackage{varwidth}
|
|
1090
1112
|
\\usepackage[normalem]{ulem}
|
|
1091
|
-
\\definecolor{StudioAnnotationBg}{HTML}{
|
|
1092
|
-
\\definecolor{StudioAnnotationBorder}{HTML}{
|
|
1093
|
-
\\definecolor{StudioAnnotationText}{HTML}{
|
|
1113
|
+
\\definecolor{StudioAnnotationBg}{HTML}{${annotationColors.bg}}
|
|
1114
|
+
\\definecolor{StudioAnnotationBorder}{HTML}{${annotationColors.border}}
|
|
1115
|
+
\\definecolor{StudioAnnotationText}{HTML}{${annotationColors.text}}
|
|
1094
1116
|
\\definecolor{StudioCodeBlockBg}{HTML}{F6F8FA}
|
|
1095
1117
|
\\definecolor{StudioDiffAddText}{HTML}{1A7F37}
|
|
1096
1118
|
\\definecolor{StudioDiffDelText}{HTML}{CF222E}
|
|
@@ -6501,9 +6523,20 @@ ${literalPdfConfig.fontSizeCommand}\\section*{${title.replace(/[{}\\]/g, "").tri
|
|
|
6501
6523
|
function replaceStudioAnnotationMarkersInGeneratedLatex(latex: string): string {
|
|
6502
6524
|
const lines = String(latex ?? "").split("\n");
|
|
6503
6525
|
const out: string[] = [];
|
|
6526
|
+
const pendingTextLines: string[] = [];
|
|
6504
6527
|
const rawEnvStack: string[] = [];
|
|
6505
6528
|
const rawEnvNames = new Set(["verbatim", "Verbatim", "Highlighting", "lstlisting"]);
|
|
6506
6529
|
|
|
6530
|
+
const hasRawEnvBoundary = (line: string): boolean => {
|
|
6531
|
+
const envPattern = /\\(begin|end)\{([^}]+)\}/g;
|
|
6532
|
+
let match: RegExpExecArray | null;
|
|
6533
|
+
while ((match = envPattern.exec(line)) !== null) {
|
|
6534
|
+
const envName = match[2];
|
|
6535
|
+
if (envName && rawEnvNames.has(envName)) return true;
|
|
6536
|
+
}
|
|
6537
|
+
return false;
|
|
6538
|
+
};
|
|
6539
|
+
|
|
6507
6540
|
const updateRawEnvStack = (line: string) => {
|
|
6508
6541
|
const envPattern = /\\(begin|end)\{([^}]+)\}/g;
|
|
6509
6542
|
let match: RegExpExecArray | null;
|
|
@@ -6524,6 +6557,12 @@ function replaceStudioAnnotationMarkersInGeneratedLatex(latex: string): string {
|
|
|
6524
6557
|
}
|
|
6525
6558
|
};
|
|
6526
6559
|
|
|
6560
|
+
const flushPendingText = () => {
|
|
6561
|
+
if (pendingTextLines.length === 0) return;
|
|
6562
|
+
out.push(replaceStudioAnnotationMarkersForPdfInSegment(pendingTextLines.join("\n")));
|
|
6563
|
+
pendingTextLines.length = 0;
|
|
6564
|
+
};
|
|
6565
|
+
|
|
6527
6566
|
for (const line of lines) {
|
|
6528
6567
|
if (rawEnvStack.length > 0) {
|
|
6529
6568
|
out.push(line);
|
|
@@ -6531,10 +6570,17 @@ function replaceStudioAnnotationMarkersInGeneratedLatex(latex: string): string {
|
|
|
6531
6570
|
continue;
|
|
6532
6571
|
}
|
|
6533
6572
|
|
|
6534
|
-
|
|
6535
|
-
|
|
6573
|
+
if (hasRawEnvBoundary(line)) {
|
|
6574
|
+
flushPendingText();
|
|
6575
|
+
out.push(line);
|
|
6576
|
+
updateRawEnvStack(line);
|
|
6577
|
+
continue;
|
|
6578
|
+
}
|
|
6579
|
+
|
|
6580
|
+
pendingTextLines.push(line);
|
|
6536
6581
|
}
|
|
6537
6582
|
|
|
6583
|
+
flushPendingText();
|
|
6538
6584
|
return out.join("\n");
|
|
6539
6585
|
}
|
|
6540
6586
|
|
|
@@ -6727,6 +6773,7 @@ async function renderStudioPdfFromGeneratedLatex(
|
|
|
6727
6773
|
alignedImageBlocks: StudioPdfAlignedImageBlock[] = [],
|
|
6728
6774
|
pdfOptions?: StudioPdfRenderOptions,
|
|
6729
6775
|
extraPreamble = "",
|
|
6776
|
+
themeStyle?: StudioThemeStyle,
|
|
6730
6777
|
): Promise<{ pdf: Buffer; warning?: string }> {
|
|
6731
6778
|
const tempDir = join(tmpdir(), `pi-studio-pdf-${Date.now()}-${randomUUID()}`);
|
|
6732
6779
|
const preamblePath = join(tempDir, "_pdf_preamble.tex");
|
|
@@ -6734,7 +6781,7 @@ async function renderStudioPdfFromGeneratedLatex(
|
|
|
6734
6781
|
const outputPath = join(tempDir, "studio-export.pdf");
|
|
6735
6782
|
|
|
6736
6783
|
await mkdir(tempDir, { recursive: true });
|
|
6737
|
-
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions, extraPreamble), "utf-8");
|
|
6784
|
+
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions, extraPreamble, themeStyle), "utf-8");
|
|
6738
6785
|
|
|
6739
6786
|
const pandocArgs = [
|
|
6740
6787
|
"-f", inputFormat,
|
|
@@ -6805,6 +6852,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
6805
6852
|
editorPdfLanguage?: string,
|
|
6806
6853
|
sourcePath?: string,
|
|
6807
6854
|
pdfOptions?: StudioPdfRenderOptions,
|
|
6855
|
+
themeStyle?: StudioThemeStyle,
|
|
6808
6856
|
): Promise<{ pdf: Buffer; warning?: string }> {
|
|
6809
6857
|
const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
|
|
6810
6858
|
const pdfEngine = process.env.PANDOC_PDF_ENGINE?.trim() || "xelatex";
|
|
@@ -6842,7 +6890,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
6842
6890
|
const outputPath = join(tempDir, "studio-export.pdf");
|
|
6843
6891
|
|
|
6844
6892
|
await mkdir(tempDir, { recursive: true });
|
|
6845
|
-
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions), "utf-8");
|
|
6893
|
+
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions, "", themeStyle), "utf-8");
|
|
6846
6894
|
|
|
6847
6895
|
const hasYamlHeaderIncludesForPdf = inputFormat !== "latex" && hasStudioYamlHeaderIncludes(markdownForPdf);
|
|
6848
6896
|
const headerIncludeArgs = hasYamlHeaderIncludesForPdf ? [] : ["--include-in-header", preamblePath];
|
|
@@ -6895,6 +6943,8 @@ async function renderStudioPdfWithPandoc(
|
|
|
6895
6943
|
[],
|
|
6896
6944
|
[],
|
|
6897
6945
|
pdfOptions,
|
|
6946
|
+
"",
|
|
6947
|
+
themeStyle,
|
|
6898
6948
|
);
|
|
6899
6949
|
}
|
|
6900
6950
|
|
|
@@ -6915,6 +6965,8 @@ async function renderStudioPdfWithPandoc(
|
|
|
6915
6965
|
[],
|
|
6916
6966
|
[],
|
|
6917
6967
|
pdfOptions,
|
|
6968
|
+
"",
|
|
6969
|
+
themeStyle,
|
|
6918
6970
|
);
|
|
6919
6971
|
} catch {
|
|
6920
6972
|
const fenced = parseStudioSingleFencedCodeBlock(diffMarkdown);
|
|
@@ -6941,7 +6993,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
6941
6993
|
const outputPath = join(tempDir, "studio-export.pdf");
|
|
6942
6994
|
|
|
6943
6995
|
await mkdir(tempDir, { recursive: true });
|
|
6944
|
-
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions, extraPdfPreamble), "utf-8");
|
|
6996
|
+
await writeFile(preamblePath, buildStudioPdfPreamble(pdfOptions, extraPdfPreamble, themeStyle), "utf-8");
|
|
6945
6997
|
|
|
6946
6998
|
const mermaidPrepared: StudioMermaidPdfPreprocessResult = isLatex
|
|
6947
6999
|
? { markdown: normalizedMarkdownBody, found: 0, replaced: 0, failed: 0, missingCli: false }
|
|
@@ -6964,6 +7016,7 @@ async function renderStudioPdfWithPandoc(
|
|
|
6964
7016
|
pdfAlignedImageTransform.blocks,
|
|
6965
7017
|
pdfOptions,
|
|
6966
7018
|
extraPdfPreamble,
|
|
7019
|
+
themeStyle,
|
|
6967
7020
|
);
|
|
6968
7021
|
await rm(tempDir, { recursive: true, force: true }).catch(() => undefined);
|
|
6969
7022
|
return { pdf: rendered.pdf, warning: mermaidPrepared.warning ?? rendered.warning };
|
|
@@ -8378,6 +8431,16 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
|
|
|
8378
8431
|
}
|
|
8379
8432
|
}
|
|
8380
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
|
+
|
|
8381
8444
|
if (msg.type === "completion_suggestion_request" && typeof msg.requestId === "string" && typeof msg.text === "string") {
|
|
8382
8445
|
const textLength = msg.text.length;
|
|
8383
8446
|
const rawStart = typeof msg.selectionStart === "number" && Number.isFinite(msg.selectionStart) ? msg.selectionStart : textLength;
|
|
@@ -10230,6 +10293,7 @@ function buildStudioHtml(
|
|
|
10230
10293
|
const initialModel = escapeHtmlForInline(initialModelLabel ?? "none");
|
|
10231
10294
|
const initialTerminal = escapeHtmlForInline(initialTerminalLabel ?? "unknown");
|
|
10232
10295
|
const initialTerminalDetailAttr = escapeHtmlForInline(initialTerminalDetail ?? initialTerminalLabel ?? "unknown");
|
|
10296
|
+
const initialTheme = escapeHtmlForInline(theme?.name ?? "theme");
|
|
10233
10297
|
const initialContextTokens =
|
|
10234
10298
|
typeof initialContextUsage?.tokens === "number" && Number.isFinite(initialContextUsage.tokens)
|
|
10235
10299
|
? String(initialContextUsage.tokens)
|
|
@@ -10297,7 +10361,7 @@ ${cssVarsBlock}
|
|
|
10297
10361
|
</style>
|
|
10298
10362
|
<link rel="stylesheet" href="${stylesheetHref}" />
|
|
10299
10363
|
</head>
|
|
10300
|
-
<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}">
|
|
10301
10365
|
<header>
|
|
10302
10366
|
<h1><span class="app-logo" aria-hidden="true">π</span> Studio <span class="app-subtitle">${appSubtitle}</span></h1>
|
|
10303
10367
|
<div class="controls">
|
|
@@ -10514,7 +10578,7 @@ ${cssVarsBlock}
|
|
|
10514
10578
|
<section id="rightPane">
|
|
10515
10579
|
<div id="rightSectionHeader" class="section-header">
|
|
10516
10580
|
<div class="section-header-main">
|
|
10517
|
-
<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.">
|
|
10518
10582
|
<option value="markdown">Response (Raw)</option>
|
|
10519
10583
|
<option value="preview" selected>Response (Preview)</option>
|
|
10520
10584
|
<option value="editor-preview">Editor (Preview)</option>
|
|
@@ -10590,7 +10654,11 @@ ${cssVarsBlock}
|
|
|
10590
10654
|
<span id="statusLine"><span id="statusSpinner" aria-hidden="true"> </span><span id="status">Booting studio…</span></span>
|
|
10591
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>
|
|
10592
10656
|
<div id="footerModelMenu" class="footer-model-menu" hidden></div>
|
|
10593
|
-
<
|
|
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>
|
|
10594
10662
|
</footer>
|
|
10595
10663
|
|
|
10596
10664
|
<div id="shortcutsOverlay" class="shortcuts-overlay" hidden>
|
|
@@ -10608,7 +10676,9 @@ ${cssVarsBlock}
|
|
|
10608
10676
|
<dl>
|
|
10609
10677
|
<div><dt>F6</dt><dd>Switch between editor and right pane</dd></div>
|
|
10610
10678
|
<div><dt>F7 / Shift+F7</dt><dd>Cycle the active pane's view</dd></div>
|
|
10611
|
-
<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>
|
|
10681
|
+
<div><dt>Cmd/Ctrl+Alt+E</dt><dd>Switch the right pane directly to Editor Preview</dd></div>
|
|
10612
10682
|
<div><dt>Cmd/Ctrl+Alt+W</dt><dd>Switch the right pane directly to Working</dd></div>
|
|
10613
10683
|
<div><dt>F8</dt><dd>Focus editor text</dd></div>
|
|
10614
10684
|
<div><dt>Shift+F8</dt><dd>Focus right-pane content</dd></div>
|
|
@@ -11722,6 +11792,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
11722
11792
|
}
|
|
11723
11793
|
: null;
|
|
11724
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
|
+
|
|
11725
11814
|
const broadcastState = () => {
|
|
11726
11815
|
terminalSessionLabel = buildTerminalSessionLabel(studioCwd, getSessionNameSafe());
|
|
11727
11816
|
terminalSessionDetail = buildTerminalSessionDetail(studioCwd, getSessionNameSafe());
|
|
@@ -11740,6 +11829,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
11740
11829
|
thinkingLevel: getThinkingLevelSafe() ?? "off",
|
|
11741
11830
|
piModels: modelOptions,
|
|
11742
11831
|
suggestionModels: modelOptions,
|
|
11832
|
+
currentTheme: getCurrentStudioThemeDescriptor(),
|
|
11833
|
+
piThemes: getStudioThemeOptions(),
|
|
11743
11834
|
terminalSessionLabel,
|
|
11744
11835
|
terminalSessionDetail,
|
|
11745
11836
|
contextTokens: contextUsageSnapshot.tokens,
|
|
@@ -12039,6 +12130,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
12039
12130
|
thinkingLevel: getThinkingLevelSafe() ?? "off",
|
|
12040
12131
|
piModels: getStudioModelOptions(),
|
|
12041
12132
|
suggestionModels: getStudioModelOptions(),
|
|
12133
|
+
currentTheme: getCurrentStudioThemeDescriptor(),
|
|
12134
|
+
piThemes: getStudioThemeOptions(),
|
|
12042
12135
|
terminalSessionLabel,
|
|
12043
12136
|
terminalSessionDetail,
|
|
12044
12137
|
contextTokens: contextUsageSnapshot.tokens,
|
|
@@ -12098,6 +12191,36 @@ export default function (pi: ExtensionAPI) {
|
|
|
12098
12191
|
return;
|
|
12099
12192
|
}
|
|
12100
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
|
+
|
|
12101
12224
|
if (msg.type === "get_latest_response") {
|
|
12102
12225
|
if (!lastStudioResponse) {
|
|
12103
12226
|
sendToClient(client, { type: "info", message: "No latest assistant response is available yet." });
|
|
@@ -13553,7 +13676,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
13553
13676
|
const filename = sanitizePdfFilename(requestedFilename || (isLatex ? "studio-latex-preview.pdf" : "studio-preview.pdf"));
|
|
13554
13677
|
|
|
13555
13678
|
try {
|
|
13556
|
-
const { pdf, warning } = await renderStudioPdfWithPandoc(markdown, isLatex, resourcePath, editorPdfLanguage, sourcePath || undefined);
|
|
13679
|
+
const { pdf, warning } = await renderStudioPdfWithPandoc(markdown, isLatex, resourcePath, editorPdfLanguage, sourcePath || undefined, undefined, getStudioThemeStyle(lastCommandCtx?.ui?.theme));
|
|
13557
13680
|
const writeResult = writeStudioPreviewExportFile(buildStudioPreviewExportPath(sourcePath || undefined, userResourceDir || undefined, studioCwd, filename), pdf);
|
|
13558
13681
|
const exportId = storePreparedPdfExport(pdf, filename, warning, writeResult.filePath ?? undefined);
|
|
13559
13682
|
const token = serverState?.token ?? "";
|
|
@@ -14287,7 +14410,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14287
14410
|
lastThemeVarsJson = json;
|
|
14288
14411
|
syncCmuxStudioStatus();
|
|
14289
14412
|
for (const client of serverState.clients) {
|
|
14290
|
-
sendToClient(client, { type: "theme_update", vars });
|
|
14413
|
+
sendToClient(client, { type: "theme_update", vars, currentTheme: getCurrentStudioThemeDescriptor(), piThemes: getStudioThemeOptions() });
|
|
14291
14414
|
}
|
|
14292
14415
|
}
|
|
14293
14416
|
} catch {
|
|
@@ -14984,6 +15107,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
14984
15107
|
editorPdfLanguage,
|
|
14985
15108
|
source.sourcePath,
|
|
14986
15109
|
pdfOptions,
|
|
15110
|
+
getStudioThemeStyle(ctx.ui.theme),
|
|
14987
15111
|
);
|
|
14988
15112
|
await writeFile(source.outputPath, pdf);
|
|
14989
15113
|
const openError = await maybeOpenStudioExportPath(source.outputPath, params.open);
|
|
@@ -15296,6 +15420,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
15296
15420
|
editorPdfLanguage,
|
|
15297
15421
|
undefined,
|
|
15298
15422
|
pdfOptions,
|
|
15423
|
+
getStudioThemeStyle(ctx.ui.theme),
|
|
15299
15424
|
);
|
|
15300
15425
|
await writeFile(outputPath, pdf);
|
|
15301
15426
|
|
|
@@ -15354,6 +15479,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
15354
15479
|
editorPdfLanguage,
|
|
15355
15480
|
file.resolvedPath,
|
|
15356
15481
|
pdfOptions,
|
|
15482
|
+
getStudioThemeStyle(ctx.ui.theme),
|
|
15357
15483
|
);
|
|
15358
15484
|
await writeFile(outputPath, pdf);
|
|
15359
15485
|
|
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",
|