pi-studio 0.9.9 → 0.9.11

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/client/studio.css CHANGED
@@ -2456,6 +2456,39 @@
2456
2456
  overflow-x: auto;
2457
2457
  }
2458
2458
 
2459
+ .rendered-markdown .mermaid-source-toolbar {
2460
+ display: flex;
2461
+ justify-content: flex-end;
2462
+ margin: 0 0 4px;
2463
+ pointer-events: none;
2464
+ }
2465
+
2466
+ .rendered-markdown .mermaid-source-toolbar .studio-copy-mermaid-source-btn {
2467
+ position: static;
2468
+ top: auto;
2469
+ right: auto;
2470
+ z-index: auto;
2471
+ padding: 2px 7px;
2472
+ border-color: transparent;
2473
+ background: transparent;
2474
+ box-shadow: none;
2475
+ font-size: 11px;
2476
+ opacity: 0.38;
2477
+ pointer-events: auto;
2478
+ }
2479
+
2480
+ .rendered-markdown .mermaid-container:hover > .studio-copy-block-btn {
2481
+ opacity: 0.38;
2482
+ }
2483
+
2484
+ .rendered-markdown .mermaid-source-toolbar .studio-copy-mermaid-source-btn:hover,
2485
+ .rendered-markdown .mermaid-source-toolbar .studio-copy-mermaid-source-btn:focus-visible {
2486
+ opacity: 1;
2487
+ color: var(--text);
2488
+ border-color: var(--control-border);
2489
+ background: var(--panel);
2490
+ }
2491
+
2459
2492
  .rendered-markdown .mermaid-container svg {
2460
2493
  max-width: 100%;
2461
2494
  height: auto;
@@ -3326,15 +3359,26 @@
3326
3359
  grid-area: hint;
3327
3360
  justify-self: end;
3328
3361
  align-self: center;
3362
+ display: inline-flex;
3363
+ align-items: center;
3364
+ gap: 6px;
3365
+ padding: 4px 8px;
3366
+ border-radius: 999px;
3367
+ border: 1px solid transparent;
3368
+ background: transparent;
3329
3369
  color: var(--studio-footer-text, var(--muted));
3330
3370
  font-size: 11px;
3371
+ line-height: 1.2;
3331
3372
  white-space: nowrap;
3332
- text-align: right;
3333
- font-style: normal;
3334
- opacity: 0.86;
3335
- display: inline-flex;
3336
- align-items: center;
3337
- gap: 8px;
3373
+ opacity: 0.9;
3374
+ }
3375
+
3376
+ .shortcut-hint:not(:disabled):hover,
3377
+ .shortcut-hint:focus-visible {
3378
+ background: var(--panel-2);
3379
+ border-color: var(--control-border);
3380
+ color: var(--text);
3381
+ opacity: 1;
3338
3382
  }
3339
3383
 
3340
3384
  .footer-compact-btn {
@@ -3369,7 +3413,8 @@
3369
3413
  overflow: hidden;
3370
3414
  }
3371
3415
 
3372
- .scratchpad-overlay {
3416
+ .scratchpad-overlay,
3417
+ .shortcuts-overlay {
3373
3418
  position: fixed;
3374
3419
  inset: 0;
3375
3420
  z-index: 50;
@@ -3381,10 +3426,28 @@
3381
3426
  backdrop-filter: blur(2px);
3382
3427
  }
3383
3428
 
3384
- .scratchpad-overlay[hidden] {
3429
+ .scratchpad-overlay[hidden],
3430
+ .shortcuts-overlay[hidden] {
3385
3431
  display: none !important;
3386
3432
  }
3387
3433
 
3434
+ .scratchpad-dialog,
3435
+ .shortcuts-dialog {
3436
+ width: min(860px, 100%);
3437
+ max-height: min(82vh, 900px);
3438
+ border: 1px solid var(--panel-border);
3439
+ border-radius: 14px;
3440
+ background: var(--panel);
3441
+ box-shadow: 0 18px 50px var(--shadow-color);
3442
+ display: flex;
3443
+ flex-direction: column;
3444
+ overflow: hidden;
3445
+ }
3446
+
3447
+ .shortcuts-dialog {
3448
+ width: min(720px, 100%);
3449
+ }
3450
+
3388
3451
  .scratchpad-dialog {
3389
3452
  width: min(860px, 100%);
3390
3453
  max-height: min(82vh, 900px);
@@ -3397,7 +3460,8 @@
3397
3460
  overflow: hidden;
3398
3461
  }
3399
3462
 
3400
- .scratchpad-header {
3463
+ .scratchpad-header,
3464
+ .shortcuts-header {
3401
3465
  display: flex;
3402
3466
  align-items: flex-start;
3403
3467
  justify-content: space-between;
@@ -3407,18 +3471,21 @@
3407
3471
  background: var(--scratchpad-header-bg, var(--panel-2));
3408
3472
  }
3409
3473
 
3410
- .scratchpad-header > div {
3474
+ .scratchpad-header > div,
3475
+ .shortcuts-header > div {
3411
3476
  flex: 1 1 auto;
3412
3477
  min-width: 0;
3413
3478
  }
3414
3479
 
3415
- .scratchpad-header h2 {
3480
+ .scratchpad-header h2,
3481
+ .shortcuts-header h2 {
3416
3482
  margin: 0;
3417
3483
  font-size: 17px;
3418
3484
  font-weight: 600;
3419
3485
  }
3420
3486
 
3421
- .scratchpad-description {
3487
+ .scratchpad-description,
3488
+ .shortcuts-description {
3422
3489
  margin: 6px 0 0;
3423
3490
  font-size: 12px;
3424
3491
  line-height: 1.45;
@@ -3426,12 +3493,60 @@
3426
3493
  max-width: none;
3427
3494
  }
3428
3495
 
3429
- .scratchpad-close-btn {
3496
+ .scratchpad-close-btn,
3497
+ .shortcuts-close-btn {
3430
3498
  padding: 6px 10px;
3431
3499
  line-height: 1;
3432
3500
  flex: 0 0 auto;
3433
3501
  }
3434
3502
 
3503
+ .shortcuts-body {
3504
+ display: grid;
3505
+ gap: 14px;
3506
+ padding: 16px 18px 18px;
3507
+ overflow: auto;
3508
+ background: var(--panel);
3509
+ }
3510
+
3511
+ .shortcuts-group h3 {
3512
+ margin: 0 0 8px;
3513
+ font-size: 12px;
3514
+ font-weight: 700;
3515
+ color: var(--studio-info-text, var(--muted));
3516
+ text-transform: uppercase;
3517
+ letter-spacing: 0.04em;
3518
+ }
3519
+
3520
+ .shortcuts-group dl {
3521
+ display: grid;
3522
+ gap: 6px;
3523
+ margin: 0;
3524
+ }
3525
+
3526
+ .shortcuts-group dl > div {
3527
+ display: grid;
3528
+ grid-template-columns: minmax(130px, max-content) minmax(0, 1fr);
3529
+ gap: 12px;
3530
+ align-items: baseline;
3531
+ padding: 6px 0;
3532
+ border-top: 1px solid var(--border-subtle);
3533
+ }
3534
+
3535
+ .shortcuts-group dt {
3536
+ margin: 0;
3537
+ font-family: var(--font-mono);
3538
+ font-size: 12px;
3539
+ color: var(--text);
3540
+ white-space: nowrap;
3541
+ }
3542
+
3543
+ .shortcuts-group dd {
3544
+ margin: 0;
3545
+ color: var(--studio-info-text, var(--muted));
3546
+ font-size: 12px;
3547
+ line-height: 1.35;
3548
+ }
3549
+
3435
3550
  .scratchpad-textarea {
3436
3551
  width: 100%;
3437
3552
  min-height: 280px;
package/index.ts CHANGED
@@ -64,6 +64,19 @@ interface StudioPromptDescriptor {
64
64
  promptTriggerText: string | null;
65
65
  }
66
66
 
67
+ interface StudioHtmlPreviewMathRenderItem {
68
+ mathId: string;
69
+ tex: string;
70
+ display: boolean;
71
+ }
72
+
73
+ interface StudioHtmlPreviewMathRenderResult {
74
+ mathId: string;
75
+ ok: boolean;
76
+ html?: string;
77
+ error?: string;
78
+ }
79
+
67
80
  interface ActiveStudioRequest extends StudioPromptDescriptor {
68
81
  id: string;
69
82
  kind: StudioRequestKind;
@@ -460,6 +473,9 @@ const REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
460
473
  const PREVIEW_RENDER_MAX_CHARS = 400_000;
461
474
  const PDF_EXPORT_MAX_CHARS = 400_000;
462
475
  const HTML_EXPORT_MAX_CHARS = 400_000;
476
+ const HTML_PREVIEW_MATH_RENDER_MAX_ITEMS = 250;
477
+ const HTML_PREVIEW_MATH_RENDER_ITEM_MAX_CHARS = 8_000;
478
+ const HTML_PREVIEW_MATH_RENDER_TOTAL_MAX_CHARS = 120_000;
463
479
  const STUDIO_QUIZ_SOURCE_MAX_CHARS = 80_000;
464
480
  const STUDIO_QUIZ_CONTEXT_FILE_MAX_CHARS = 14_000;
465
481
  const STUDIO_QUIZ_CONTEXT_MAX_FILES = 18;
@@ -4900,6 +4916,72 @@ function stripMathMlAnnotationTags(html: string): string {
4900
4916
  });
4901
4917
  }
4902
4918
 
4919
+ function normalizeStudioHtmlPreviewMathForPandoc(tex: string): string {
4920
+ return String(tex ?? "")
4921
+ .replace(/\r\n/g, "\n")
4922
+ .replace(/\\rm\s*\{([^{}]+)\}/g, "\\mathrm{$1}")
4923
+ .replace(/\\rm\s+([A-Za-z]+)(?=[^A-Za-z]|$)/g, "\\mathrm{$1}");
4924
+ }
4925
+
4926
+ function getStudioHtmlPreviewMathWrapperId(index: number): string {
4927
+ return `studio-html-preview-math-${Math.max(0, Math.floor(index))}`;
4928
+ }
4929
+
4930
+ function buildStudioHtmlPreviewMathPandocSource(items: StudioHtmlPreviewMathRenderItem[]): string {
4931
+ return items.map((item, index) => {
4932
+ const wrapperId = getStudioHtmlPreviewMathWrapperId(index);
4933
+ const tex = normalizeStudioHtmlPreviewMathForPandoc(item.tex);
4934
+ const mathSource = item.display ? `\\[\n${tex}\n\\]` : `\\(${tex}\\)`;
4935
+ return `:::: {#${wrapperId} .studio-html-preview-math-render-item}\n${mathSource}\n::::`;
4936
+ }).join("\n\n");
4937
+ }
4938
+
4939
+ function extractStudioHtmlPreviewMathHtml(renderedHtml: string, wrapperId: string): string {
4940
+ const idPattern = escapeStudioRegExpLiteral(wrapperId);
4941
+ const wrapperPattern = new RegExp(`<div\\b(?=[^>]*\\bid="${idPattern}")[^>]*>([\\s\\S]*?)<\\/div>`, "i");
4942
+ const wrapperMatch = String(renderedHtml ?? "").match(wrapperPattern);
4943
+ const wrapperHtml = wrapperMatch ? String(wrapperMatch[1] ?? "") : "";
4944
+ const mathMatch = wrapperHtml.match(/<math\b[\s\S]*?<\/math>/i);
4945
+ return mathMatch ? stripMathMlAnnotationTags(mathMatch[0]) : "";
4946
+ }
4947
+
4948
+ async function renderStudioHtmlPreviewMathWithPandoc(items: StudioHtmlPreviewMathRenderItem[]): Promise<StudioHtmlPreviewMathRenderResult[]> {
4949
+ if (items.length === 0) return [];
4950
+ const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
4951
+ const inputFormat = "markdown+tex_math_dollars+tex_math_single_backslash+tex_math_double_backslash";
4952
+ const args = ["-f", inputFormat, "-t", "html5", "--mathml", "--wrap=none"];
4953
+ const source = buildStudioHtmlPreviewMathPandocSource(items);
4954
+ const pandocResult = await runStudioSubprocess(pandocCommand, args, {
4955
+ input: source,
4956
+ timeoutMs: STUDIO_PANDOC_TIMEOUT_MS,
4957
+ stdoutMaxBytes: Math.min(STUDIO_HTML_RENDER_OUTPUT_MAX_BYTES, 10_000_000),
4958
+ label: "pandoc HTML preview math render",
4959
+ notFoundMessage: "pandoc was not found. Install pandoc or set PANDOC_PATH to the pandoc binary.",
4960
+ });
4961
+ if (pandocResult.code !== 0) {
4962
+ throw new Error(`pandoc math render failed with exit code ${pandocResult.code}${pandocResult.stderr ? `: ${pandocResult.stderr}` : ""}`);
4963
+ }
4964
+ if (pandocResult.stdoutTruncated) {
4965
+ throw new Error("pandoc math render output exceeded Studio's size limit.");
4966
+ }
4967
+
4968
+ return items.map((item, index) => {
4969
+ const html = extractStudioHtmlPreviewMathHtml(pandocResult.stdout, getStudioHtmlPreviewMathWrapperId(index));
4970
+ if (!html) {
4971
+ return {
4972
+ mathId: item.mathId,
4973
+ ok: false,
4974
+ error: "Pandoc did not render this expression as MathML.",
4975
+ };
4976
+ }
4977
+ return {
4978
+ mathId: item.mathId,
4979
+ ok: true,
4980
+ html,
4981
+ };
4982
+ });
4983
+ }
4984
+
4903
4985
  function normalizeObsidianImages(markdown: string): string {
4904
4986
  // Use angle-bracket destinations so paths with spaces/special chars are safe for Pandoc
4905
4987
  return markdown
@@ -8675,7 +8757,7 @@ ${cssVarsBlock}
8675
8757
  <label class="file-label" title="Load a local file into editor text.">Load file content<input id="fileInput" type="file" accept=".md,.markdown,.mdx,.qmd,.js,.mjs,.cjs,.jsx,.ts,.mts,.cts,.tsx,.py,.pyw,.sh,.bash,.zsh,.json,.jsonc,.json5,.rs,.c,.h,.cpp,.cxx,.cc,.hpp,.hxx,.jl,.f90,.f95,.f03,.f,.for,.r,.R,.m,.tex,.latex,.diff,.patch,.java,.go,.rb,.swift,.html,.htm,.css,.xml,.yaml,.yml,.toml,.lua,.txt,.rst,.adoc" /></label>
8676
8758
  <button id="loadGitDiffBtn" type="button" title="Load the current git diff from the Studio context into the editor.">Load git diff</button>
8677
8759
  <button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
8678
- <button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide secondary Studio controls.">Zen</button>
8760
+ <button id="zenModeBtn" class="zen-mode-btn" type="button" title="Hide secondary Studio controls. Shortcut: F9.">Zen</button>
8679
8761
  </div>
8680
8762
  </header>
8681
8763
 
@@ -8683,7 +8765,7 @@ ${cssVarsBlock}
8683
8765
  <section id="leftPane">
8684
8766
  <div id="leftSectionHeader" class="section-header">
8685
8767
  <div class="section-header-main">
8686
- <select id="editorViewSelect" aria-label="Editor view mode">
8768
+ <select id="editorViewSelect" aria-label="Editor view mode" title="Editor view mode. Shortcut: F7 when the editor pane is active; F6 switches panes.">
8687
8769
  <option value="markdown" selected>Editor (Raw)</option>
8688
8770
  <option value="preview">Editor (Preview)</option>
8689
8771
  </select>
@@ -8858,7 +8940,7 @@ ${cssVarsBlock}
8858
8940
  <section id="rightPane">
8859
8941
  <div id="rightSectionHeader" class="section-header">
8860
8942
  <div class="section-header-main">
8861
- <select id="rightViewSelect" aria-label="Response view mode">
8943
+ <select id="rightViewSelect" aria-label="Response view mode" title="Right pane view mode. Shortcut: F7 when the right pane is active; F6 switches panes.">
8862
8944
  <option value="markdown">Response (Raw)</option>
8863
8945
  <option value="preview" selected>Response (Preview)</option>
8864
8946
  <option value="editor-preview">Editor (Preview)</option>
@@ -8929,9 +9011,50 @@ ${cssVarsBlock}
8929
9011
  <footer>
8930
9012
  <span id="statusLine"><span id="statusSpinner" aria-hidden="true"> </span><span id="status">Booting studio…</span></span>
8931
9013
  <span id="footerMeta" class="footer-meta"><span id="footerMetaText" class="footer-meta-text"><span id="footerMetaModel" class="footer-meta-part footer-meta-model">${initialModel}</span><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>
8932
- <span class="shortcut-hint">Focus pane: F10 (or Cmd/Ctrl+Esc) to toggle · Save editor: Cmd/Ctrl+S · Run / queue steering: Cmd/Ctrl+Enter · Stop request: Esc</span>
9014
+ <button id="shortcutsBtn" class="shortcut-hint" type="button" title="Show Studio keyboard shortcuts. Press ? when not editing text.">Shortcuts (?)</button>
8933
9015
  </footer>
8934
9016
 
9017
+ <div id="shortcutsOverlay" class="shortcuts-overlay" hidden>
9018
+ <div id="shortcutsDialog" class="shortcuts-dialog" role="dialog" aria-modal="true" aria-labelledby="shortcutsTitle">
9019
+ <div class="shortcuts-header">
9020
+ <div>
9021
+ <h2 id="shortcutsTitle">Keyboard shortcuts</h2>
9022
+ <p class="shortcuts-description">Studio navigation and high-frequency actions.</p>
9023
+ </div>
9024
+ <button id="shortcutsCloseBtn" class="shortcuts-close-btn" type="button" aria-label="Close keyboard shortcuts">Close</button>
9025
+ </div>
9026
+ <div class="shortcuts-body">
9027
+ <section class="shortcuts-group">
9028
+ <h3>Navigation</h3>
9029
+ <dl>
9030
+ <div><dt>F6</dt><dd>Switch between editor and right pane</dd></div>
9031
+ <div><dt>F7 / Shift+F7</dt><dd>Cycle the active pane's view</dd></div>
9032
+ <div><dt>F8</dt><dd>Focus editor text</dd></div>
9033
+ <div><dt>Shift+F8</dt><dd>Focus right-pane content</dd></div>
9034
+ <div><dt>F9</dt><dd>Toggle Zen mode</dd></div>
9035
+ <div><dt>F10</dt><dd>Focus or unfocus the active pane</dd></div>
9036
+ <div><dt>Esc</dt><dd>Close overlays, exit pane focus, or stop an active request</dd></div>
9037
+ <div><dt>?</dt><dd>Show keyboard shortcuts when not editing text</dd></div>
9038
+ </dl>
9039
+ </section>
9040
+ <section class="shortcuts-group">
9041
+ <h3>Editor</h3>
9042
+ <dl>
9043
+ <div><dt>Cmd/Ctrl+S</dt><dd>Save editor</dd></div>
9044
+ <div><dt>Cmd/Ctrl+Enter</dt><dd>Run editor text, or queue steering during an active run</dd></div>
9045
+ <div><dt>Tab / Shift+Tab</dt><dd>Indent or unindent selected editor text</dd></div>
9046
+ </dl>
9047
+ </section>
9048
+ <section class="shortcuts-group">
9049
+ <h3>REPL</h3>
9050
+ <dl>
9051
+ <div><dt>Cmd/Ctrl+Shift+Enter</dt><dd>Send selection, chunks, or editor text to the active REPL when the right pane is REPL</dd></div>
9052
+ </dl>
9053
+ </section>
9054
+ </div>
9055
+ </div>
9056
+ </div>
9057
+
8935
9058
  <div id="scratchpadOverlay" class="scratchpad-overlay" hidden>
8936
9059
  <div id="scratchpadDialog" class="scratchpad-dialog" role="dialog" aria-modal="true" aria-labelledby="scratchpadTitle">
8937
9060
  <div class="scratchpad-header">
@@ -11438,6 +11561,84 @@ export default function (pi: ExtensionAPI) {
11438
11561
  }
11439
11562
  };
11440
11563
 
11564
+ const handleRenderMathRequest = async (req: IncomingMessage, res: ServerResponse) => {
11565
+ let rawBody = "";
11566
+ try {
11567
+ rawBody = await readRequestBody(req, REQUEST_BODY_MAX_BYTES);
11568
+ } catch (error) {
11569
+ const message = error instanceof Error ? error.message : String(error);
11570
+ const status = message.includes("exceeds") ? 413 : 400;
11571
+ respondJson(res, status, { ok: false, error: message });
11572
+ return;
11573
+ }
11574
+
11575
+ let parsedBody: unknown;
11576
+ try {
11577
+ parsedBody = rawBody ? JSON.parse(rawBody) : {};
11578
+ } catch {
11579
+ respondJson(res, 400, { ok: false, error: "Invalid JSON body." });
11580
+ return;
11581
+ }
11582
+
11583
+ const rawItems =
11584
+ parsedBody && typeof parsedBody === "object" && Array.isArray((parsedBody as { items?: unknown }).items)
11585
+ ? (parsedBody as { items: unknown[] }).items
11586
+ : null;
11587
+ if (!rawItems) {
11588
+ respondJson(res, 400, { ok: false, error: "Missing math items array in request body." });
11589
+ return;
11590
+ }
11591
+ if (rawItems.length > HTML_PREVIEW_MATH_RENDER_MAX_ITEMS) {
11592
+ respondJson(res, 413, {
11593
+ ok: false,
11594
+ error: `HTML preview math render accepts at most ${HTML_PREVIEW_MATH_RENDER_MAX_ITEMS} items per request.`,
11595
+ });
11596
+ return;
11597
+ }
11598
+
11599
+ const items: StudioHtmlPreviewMathRenderItem[] = [];
11600
+ let totalChars = 0;
11601
+ for (const rawItem of rawItems) {
11602
+ const item = rawItem && typeof rawItem === "object" ? rawItem as { mathId?: unknown; tex?: unknown; display?: unknown } : null;
11603
+ const mathId = typeof item?.mathId === "string" ? item.mathId.trim() : "";
11604
+ const tex = typeof item?.tex === "string" ? item.tex : "";
11605
+ if (!mathId || !tex.trim()) continue;
11606
+ if (mathId.length > 160) {
11607
+ respondJson(res, 400, { ok: false, error: "Math item id is too long." });
11608
+ return;
11609
+ }
11610
+ if (tex.length > HTML_PREVIEW_MATH_RENDER_ITEM_MAX_CHARS) {
11611
+ respondJson(res, 413, {
11612
+ ok: false,
11613
+ error: `A math expression exceeds ${HTML_PREVIEW_MATH_RENDER_ITEM_MAX_CHARS} characters.`,
11614
+ });
11615
+ return;
11616
+ }
11617
+ totalChars += tex.length;
11618
+ if (totalChars > HTML_PREVIEW_MATH_RENDER_TOTAL_MAX_CHARS) {
11619
+ respondJson(res, 413, {
11620
+ ok: false,
11621
+ error: `Math render text exceeds ${HTML_PREVIEW_MATH_RENDER_TOTAL_MAX_CHARS} characters.`,
11622
+ });
11623
+ return;
11624
+ }
11625
+ items.push({ mathId, tex, display: Boolean(item?.display) });
11626
+ }
11627
+
11628
+ if (items.length === 0) {
11629
+ respondJson(res, 400, { ok: false, error: "No valid math items to render." });
11630
+ return;
11631
+ }
11632
+
11633
+ try {
11634
+ const results = await renderStudioHtmlPreviewMathWithPandoc(items);
11635
+ respondJson(res, 200, { ok: true, renderer: "pandoc", results });
11636
+ } catch (error) {
11637
+ const message = error instanceof Error ? error.message : String(error);
11638
+ respondJson(res, 500, { ok: false, error: `Math render failed: ${message}` });
11639
+ }
11640
+ };
11641
+
11441
11642
  const handleExportPdfRequest = async (req: IncomingMessage, res: ServerResponse) => {
11442
11643
  let rawBody = "";
11443
11644
  try {
@@ -11803,6 +12004,29 @@ export default function (pi: ExtensionAPI) {
11803
12004
  return;
11804
12005
  }
11805
12006
 
12007
+ if (requestUrl.pathname === "/render-math") {
12008
+ const token = requestUrl.searchParams.get("token") ?? "";
12009
+ if (token !== serverState.token) {
12010
+ respondJson(res, 403, { ok: false, error: "Invalid or expired studio token. Re-run /studio." });
12011
+ return;
12012
+ }
12013
+
12014
+ const method = (req.method ?? "GET").toUpperCase();
12015
+ if (method !== "POST") {
12016
+ res.setHeader("Allow", "POST");
12017
+ respondJson(res, 405, { ok: false, error: "Method not allowed. Use POST." });
12018
+ return;
12019
+ }
12020
+
12021
+ void handleRenderMathRequest(req, res).catch((error) => {
12022
+ respondJson(res, 500, {
12023
+ ok: false,
12024
+ error: `Math render failed: ${error instanceof Error ? error.message : String(error)}`,
12025
+ });
12026
+ });
12027
+ return;
12028
+ }
12029
+
11806
12030
  if (requestUrl.pathname === "/export-pdf") {
11807
12031
  const token = requestUrl.searchParams.get("token") ?? "";
11808
12032
  if (token !== serverState.token) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.9.9",
3
+ "version": "0.9.11",
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",