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 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
  ![Pi Studio workspace (dark)](./assets/screenshots/dark-workspace.png)
14
16
 
15
- **Light**
17
+ **Light workspace**
16
18
 
17
19
  ![Pi Studio workspace (light)](./assets/screenshots/light-workspace.png)
18
20
 
21
+ **Zen mode**
22
+
23
+ ![Pi Studio Zen mode (dark)](./assets/screenshots/dark-zen.png)
24
+
25
+ **Zen mode with a custom theme and rendered code preview**
26
+
27
+ ![Pi Studio Zen mode with custom theme and rendered code preview](./assets/screenshots/theme-zen-code.png)
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` switch directly between Response Preview, Editor Preview, and Working
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 a compact-context action
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.
@@ -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
+ ![Gradient descent contour plot](generated/gradient_descent_contour.png){ 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
+ ![Gradient descent convergence plot](generated/gradient_descent_convergence.png){ 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
@@ -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 normalizeRightViewValue(nextView) {
273
+ function canonicalRightViewValue(nextView) {
246
274
  const raw = String(nextView || "").trim();
247
- const normalized = raw === "preview"
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
- if (isEditorOnlyMode && normalized !== "editor-preview" && normalized !== "files" && normalized !== "changes" && normalized !== "repl") {
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 && !editorOnlyAllowed.has(option.value);
314
+ option.disabled = isEditorOnlyMode && !EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(option.value);
270
315
  });
271
316
  rightViewSelect.title = isEditorOnlyMode
272
- ? "Editor-only views: editor preview, Changes, Files, or REPL. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+P or Cmd/Ctrl+Alt+E switches directly to Preview."
273
- : "Right pane view mode. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+P switches directly to Response Preview; Cmd/Ctrl+Alt+E switches directly to Editor Preview; Cmd/Ctrl+Alt+W switches directly to Working.";
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 switchRightPaneToPrimaryPreview() {
3799
- const targetView = isEditorOnlyMode ? "editor-preview" : "preview";
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(targetView);
3929
+ setRightView(requestedView);
3802
3930
  scheduleStudioScrollablePositionRestore(snapshot);
3803
- const label = rightViewSelect && rightViewSelect.selectedOptions && rightViewSelect.selectedOptions[0]
3804
- ? rightViewSelect.selectedOptions[0].textContent
3805
- : (isEditorOnlyMode ? "Editor (Preview)" : "Response (Preview)");
3806
- setStatus("Right pane view: " + String(label || "Preview") + ".");
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
- if (isEditorOnlyMode) {
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
- const snapshot = snapshotStudioScrollablePositions();
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[hidden] {
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
- .shortcut-hint {
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 .shortcut-hint {
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
- .shortcut-hint {
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+P switches directly to Response Preview; Cmd/Ctrl+Alt+E switches directly to Editor Preview; Cmd/Ctrl+Alt+W switches directly to Working.">
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
- <button id="shortcutsBtn" class="shortcut-hint" type="button" title="Show Studio keyboard shortcuts. Press ? when not editing text.">Shortcuts (?)</button>
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+P</dt><dd>Switch the right pane directly to Response Preview</dd></div>
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.33",
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",