pi-studio 0.1.8 → 0.2.0

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
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to `pi-studio` are documented here.
4
4
 
5
+ ## [0.2.0] — 2026-03-02
6
+
7
+ ### Added
8
+ - Luminance-based canvas color derivation from theme surface colors — proper bg/panel/panel2 tiers instead of flat mid-tone mapping.
9
+ - Dedicated `--editor-bg` CSS variable — editor text box pushed toward white (light) for a crisp paper feel.
10
+ - `Cmd/Ctrl+Enter` keyboard shortcut to trigger "Run editor text" when editor pane is active.
11
+
12
+ ### Changed
13
+ - Renamed "Highlight markdown: On/Off" → "Syntax highlight: On/Off".
14
+ - Renamed "Editor: Markdown" / "Response: Markdown" → "Editor: Raw" / "Response: Raw" (future-proofing for non-markdown formats).
15
+ - Active pane indicator simplified to subtle border color change (removed thick top accent bar).
16
+ - Panel shadows, button hierarchy (filled accent for primary actions), heading scale, blockquote/table styling improvements.
17
+
5
18
  ## [Unreleased]
6
19
 
7
20
  ### Added
@@ -25,6 +38,7 @@ All notable changes to `pi-studio` are documented here.
25
38
  - Response-side markdown highlighting toggle (`Highlight markdown: Off|On`) in `Response: Markdown` view, with local preference persistence.
26
39
  - Markdown highlighter now applies lightweight fenced-code token colors for common languages (`js/ts`, `python`, `bash/sh`, `json`).
27
40
  - Obsidian wiki-image syntax normalization (`![[path]]`, `![[path|alt]]`) before pandoc preview rendering.
41
+ - Client-side Mermaid rendering for fenced `mermaid` code blocks in both Preview panes.
28
42
 
29
43
  ### Changed
30
44
  - Removed Annotate/Critique tabs and related mode state.
@@ -38,6 +52,10 @@ All notable changes to `pi-studio` are documented here.
38
52
  - Studio still live-updates latest response when assistant output arrives outside studio requests (e.g., manual send from pi editor).
39
53
  - Preview pane typography/style now follows the higher-fidelity `/preview-browser` rendering style more closely.
40
54
  - Preview mode now uses pandoc code highlighting output for syntax-colored code blocks.
55
+ - Preview markdown styling now maps markdown (`md*`) and syntax (`syntax*`) theme tokens for closer parity with terminal rendering.
56
+ - Theme surface mapping now uses theme-export backgrounds when available (`pageBg`, `cardBg`, `infoBg`) for clearer depth across `bg/panel/panel2`.
57
+ - Mermaid preview now uses palette-driven Mermaid defaults (base theme + theme variables) for better visual fit with active pi themes.
58
+ - Studio chrome was refined for a cleaner visual hierarchy (subtle panel shadows, primary action emphasis, lighter active-pane accent bar, softer heading scale, table striping, and tinted blockquotes).
41
59
  - Hardened Studio preview HTTP handling and added client-side preview-request timeout to avoid stuck "Rendering preview…" states.
42
60
 
43
61
  ### Fixed
@@ -47,6 +65,7 @@ All notable changes to `pi-studio` are documented here.
47
65
  - `respondText` now includes `X-Content-Type-Options: nosniff` for consistency with JSON responses.
48
66
  - If `dompurify` is unavailable, preview now falls back to escaped plain markdown instead of injecting unsanitized HTML.
49
67
  - Preview sanitization now preserves MathML profile and strips MathML annotation tags to avoid duplicate raw TeX text beside rendered equations.
68
+ - Preview now shows an inline warning when Mermaid is unavailable or diagram rendering fails, instead of failing silently.
50
69
 
51
70
  ### Changed
52
71
  - Added npm metadata fields (`repository`, `homepage`, `bugs`) so npm package page links to GitHub.
package/README.md CHANGED
@@ -33,8 +33,9 @@ Status: experimental alpha.
33
33
  - critique: **Load critique (notes)** / **Load critique (full)**
34
34
  - File actions: **Save As…**, **Save file**, **Load file in editor**
35
35
  - View toggles: `Editor: Markdown|Preview`, `Response: Markdown|Preview`
36
+ - Preview mode supports MathML equations and Mermaid fenced diagrams
36
37
  - Optional markdown highlighting toggles for editor and response markdown views (including fenced-code token colors for common languages)
37
- - Theme-aware browser UI based on current pi theme
38
+ - Theme-aware browser UI based on current pi theme, with refined surface depth and lighter visual chrome
38
39
 
39
40
  ## Commands
40
41
 
@@ -75,6 +76,9 @@ pi -e https://github.com/omaclaren/pi-studio
75
76
  - Pi Studio is currently optimized for markdown workflows (model responses, plans, and notes), including fenced code blocks. Pure code files are supported, but highlighting is tuned for markdown and fenced blocks rather than full-file language mode.
76
77
  - Studio URLs include a token query parameter; avoid sharing full Studio URLs.
77
78
  - Preview panes render markdown via `pandoc` (`gfm+tex_math_dollars` → HTML5 + MathML), including pandoc code syntax highlighting, sanitized in-browser with `dompurify`.
79
+ - Preview markdown/code colors are mapped from active theme markdown (`md*`) and syntax (`syntax*`) tokens for closer terminal-vs-browser parity.
80
+ - Mermaid fenced `mermaid` code blocks are rendered client-side in preview mode (Mermaid v11 loaded from jsDelivr), with palette-driven defaults for better theme fit.
81
+ - If Mermaid cannot load or a diagram fails to render, preview shows an inline warning and keeps source text visible.
78
82
  - Preview rendering normalizes Obsidian wiki-image syntax (`![[path]]`, `![[path|alt]]`) into standard markdown images.
79
83
  - Install pandoc for full preview rendering (`brew install pandoc` on macOS).
80
84
  - If `pandoc` is unavailable, preview falls back to plain markdown text with an inline warning.
package/index.ts CHANGED
@@ -112,6 +112,7 @@ interface StudioPalette {
112
112
  panel: string;
113
113
  panel2: string;
114
114
  border: string;
115
+ borderMuted: string;
115
116
  text: string;
116
117
  muted: string;
117
118
  accent: string;
@@ -124,6 +125,25 @@ interface StudioPalette {
124
125
  accentSoftStrong: string;
125
126
  okBorder: string;
126
127
  warnBorder: string;
128
+ mdHeading: string;
129
+ mdLink: string;
130
+ mdLinkUrl: string;
131
+ mdCode: string;
132
+ mdCodeBlock: string;
133
+ mdCodeBlockBorder: string;
134
+ mdQuote: string;
135
+ mdQuoteBorder: string;
136
+ mdHr: string;
137
+ mdListBullet: string;
138
+ syntaxComment: string;
139
+ syntaxKeyword: string;
140
+ syntaxFunction: string;
141
+ syntaxVariable: string;
142
+ syntaxString: string;
143
+ syntaxNumber: string;
144
+ syntaxType: string;
145
+ syntaxOperator: string;
146
+ syntaxPunctuation: string;
127
147
  }
128
148
 
129
149
  interface StudioThemeStyle {
@@ -136,6 +156,7 @@ const DARK_STUDIO_PALETTE: StudioPalette = {
136
156
  panel: "#171b24",
137
157
  panel2: "#11161f",
138
158
  border: "#2d3748",
159
+ borderMuted: "#242b38",
139
160
  text: "#e6edf3",
140
161
  muted: "#9aa5b1",
141
162
  accent: "#5ea1ff",
@@ -148,6 +169,25 @@ const DARK_STUDIO_PALETTE: StudioPalette = {
148
169
  accentSoftStrong: "rgba(94, 161, 255, 0.40)",
149
170
  okBorder: "rgba(115, 209, 61, 0.70)",
150
171
  warnBorder: "rgba(249, 199, 79, 0.70)",
172
+ mdHeading: "#f0c674",
173
+ mdLink: "#81a2be",
174
+ mdLinkUrl: "#666666",
175
+ mdCode: "#8abeb7",
176
+ mdCodeBlock: "#b5bd68",
177
+ mdCodeBlockBorder: "#808080",
178
+ mdQuote: "#808080",
179
+ mdQuoteBorder: "#808080",
180
+ mdHr: "#808080",
181
+ mdListBullet: "#8abeb7",
182
+ syntaxComment: "#6A9955",
183
+ syntaxKeyword: "#569CD6",
184
+ syntaxFunction: "#DCDCAA",
185
+ syntaxVariable: "#9CDCFE",
186
+ syntaxString: "#CE9178",
187
+ syntaxNumber: "#B5CEA8",
188
+ syntaxType: "#4EC9B0",
189
+ syntaxOperator: "#D4D4D4",
190
+ syntaxPunctuation: "#D4D4D4",
151
191
  };
152
192
 
153
193
  const LIGHT_STUDIO_PALETTE: StudioPalette = {
@@ -155,6 +195,7 @@ const LIGHT_STUDIO_PALETTE: StudioPalette = {
155
195
  panel: "#ffffff",
156
196
  panel2: "#f8fafc",
157
197
  border: "#d0d7de",
198
+ borderMuted: "#e0e6ee",
158
199
  text: "#1f2328",
159
200
  muted: "#57606a",
160
201
  accent: "#0969da",
@@ -167,6 +208,25 @@ const LIGHT_STUDIO_PALETTE: StudioPalette = {
167
208
  accentSoftStrong: "rgba(9, 105, 218, 0.35)",
168
209
  okBorder: "rgba(26, 127, 55, 0.55)",
169
210
  warnBorder: "rgba(154, 103, 0, 0.55)",
211
+ mdHeading: "#9a7326",
212
+ mdLink: "#547da7",
213
+ mdLinkUrl: "#767676",
214
+ mdCode: "#5a8080",
215
+ mdCodeBlock: "#588458",
216
+ mdCodeBlockBorder: "#6c6c6c",
217
+ mdQuote: "#6c6c6c",
218
+ mdQuoteBorder: "#6c6c6c",
219
+ mdHr: "#6c6c6c",
220
+ mdListBullet: "#588458",
221
+ syntaxComment: "#008000",
222
+ syntaxKeyword: "#0000FF",
223
+ syntaxFunction: "#795E26",
224
+ syntaxVariable: "#001080",
225
+ syntaxString: "#A31515",
226
+ syntaxNumber: "#098658",
227
+ syntaxType: "#267F99",
228
+ syntaxOperator: "#000000",
229
+ syntaxPunctuation: "#000000",
170
230
  };
171
231
 
172
232
  function getStudioThemeMode(theme?: Theme): StudioThemeMode {
@@ -278,6 +338,127 @@ function withAlpha(color: string, alpha: number, fallback: string): string {
278
338
  return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${clamped.toFixed(2)})`;
279
339
  }
280
340
 
341
+ function adjustBrightness(color: string, factor: number): string {
342
+ const rgb = hexToRgb(color);
343
+ if (!rgb) return color;
344
+ return rgbToHex(
345
+ Math.round(rgb.r * factor),
346
+ Math.round(rgb.g * factor),
347
+ Math.round(rgb.b * factor),
348
+ );
349
+ }
350
+
351
+ function relativeLuminance(color: string): number {
352
+ const rgb = hexToRgb(color);
353
+ if (!rgb) return 0;
354
+ return (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
355
+ }
356
+
357
+ function blendColors(a: string, b: string, t: number): string {
358
+ const rgbA = hexToRgb(a);
359
+ const rgbB = hexToRgb(b);
360
+ if (!rgbA || !rgbB) return a;
361
+ return rgbToHex(
362
+ Math.round(rgbA.r + (rgbB.r - rgbA.r) * t),
363
+ Math.round(rgbA.g + (rgbB.g - rgbA.g) * t),
364
+ Math.round(rgbA.b + (rgbB.b - rgbA.b) * t),
365
+ );
366
+ }
367
+
368
+ function deriveCanvasColors(
369
+ baseColor: string,
370
+ mode: StudioThemeMode,
371
+ ): { pageBg: string; cardBg: string; panel2: string } {
372
+ if (mode === "dark") {
373
+ const pageBg = adjustBrightness(baseColor, 0.50);
374
+ const cardBg = adjustBrightness(baseColor, 0.60);
375
+ return {
376
+ pageBg,
377
+ cardBg,
378
+ panel2: adjustBrightness(baseColor, 0.72),
379
+ };
380
+ }
381
+ const lum = relativeLuminance(baseColor);
382
+ const lighten = (c: string, amount: number): string => {
383
+ const rgb = hexToRgb(c);
384
+ if (!rgb) return c;
385
+ return rgbToHex(
386
+ Math.round(rgb.r + (255 - rgb.r) * amount),
387
+ Math.round(rgb.g + (255 - rgb.g) * amount),
388
+ Math.round(rgb.b + (255 - rgb.b) * amount),
389
+ );
390
+ };
391
+ if (lum > 0.92) {
392
+ return { pageBg: baseColor, cardBg: "#ffffff", panel2: lighten(baseColor, 0.3) };
393
+ }
394
+ return {
395
+ pageBg: lighten(baseColor, 0.6),
396
+ cardBg: lighten(baseColor, 0.93),
397
+ panel2: lighten(baseColor, 0.45),
398
+ };
399
+ }
400
+
401
+ interface ThemeExportPalette {
402
+ pageBg?: string;
403
+ cardBg?: string;
404
+ infoBg?: string;
405
+ }
406
+
407
+ const themeExportPaletteCache = new Map<string, ThemeExportPalette | null>();
408
+
409
+ function resolveThemeExportValue(
410
+ value: string | number | undefined,
411
+ vars: Record<string, string | number>,
412
+ seen: Set<string> = new Set(),
413
+ ): string | undefined {
414
+ if (value == null) return undefined;
415
+ if (typeof value === "number") return xterm256ToHex(value);
416
+
417
+ const token = value.trim();
418
+ if (!token) return undefined;
419
+ if (token.startsWith("#")) return token;
420
+
421
+ const varKey = token.startsWith("$") ? token.slice(1) : token;
422
+ if (!varKey || seen.has(varKey)) return token;
423
+
424
+ const referenced = vars[varKey];
425
+ if (referenced == null) return token;
426
+
427
+ seen.add(varKey);
428
+ return resolveThemeExportValue(referenced, vars, seen) ?? token;
429
+ }
430
+
431
+ function readThemeExportPalette(theme?: Theme): ThemeExportPalette | undefined {
432
+ const sourcePath = theme?.sourcePath?.trim();
433
+ if (!sourcePath) return undefined;
434
+
435
+ if (themeExportPaletteCache.has(sourcePath)) {
436
+ const cached = themeExportPaletteCache.get(sourcePath);
437
+ return cached ?? undefined;
438
+ }
439
+
440
+ try {
441
+ const raw = readFileSync(sourcePath, "utf-8");
442
+ const parsed = JSON.parse(raw) as {
443
+ export?: { pageBg?: string | number; cardBg?: string | number; infoBg?: string | number };
444
+ vars?: Record<string, string | number>;
445
+ };
446
+ const vars = parsed.vars ?? {};
447
+ const exportSection = parsed.export ?? {};
448
+ const resolved: ThemeExportPalette = {
449
+ pageBg: resolveThemeExportValue(exportSection.pageBg, vars),
450
+ cardBg: resolveThemeExportValue(exportSection.cardBg, vars),
451
+ infoBg: resolveThemeExportValue(exportSection.infoBg, vars),
452
+ };
453
+
454
+ themeExportPaletteCache.set(sourcePath, resolved);
455
+ return resolved;
456
+ } catch {
457
+ themeExportPaletteCache.set(sourcePath, null);
458
+ return undefined;
459
+ }
460
+ }
461
+
281
462
  function getStudioThemeStyle(theme?: Theme): StudioThemeStyle {
282
463
  const mode = getStudioThemeMode(theme);
283
464
  const fallback = mode === "light" ? LIGHT_STUDIO_PALETTE : DARK_STUDIO_PALETTE;
@@ -296,12 +477,30 @@ function getStudioThemeStyle(theme?: Theme): StudioThemeStyle {
296
477
  const warn = safeThemeColor(() => theme.getFgAnsi("warning")) ?? fallback.warn;
297
478
  const error = safeThemeColor(() => theme.getFgAnsi("error")) ?? fallback.error;
298
479
  const ok = safeThemeColor(() => theme.getFgAnsi("success")) ?? fallback.ok;
480
+ const exported = readThemeExportPalette(theme);
481
+
482
+ const surfaceBase =
483
+ safeThemeColor(() => theme.getBgAnsi("userMessageBg"))
484
+ ?? safeThemeColor(() => theme.getBgAnsi("customMessageBg"));
485
+ const derived = surfaceBase ? deriveCanvasColors(surfaceBase, mode) : undefined;
299
486
 
300
487
  const palette: StudioPalette = {
301
- bg: safeThemeColor(() => theme.getBgAnsi("customMessageBg")) ?? fallback.bg,
302
- panel: safeThemeColor(() => theme.getBgAnsi("toolPendingBg")) ?? fallback.panel,
303
- panel2: safeThemeColor(() => theme.getBgAnsi("selectedBg")) ?? fallback.panel2,
488
+ bg:
489
+ exported?.pageBg
490
+ ?? derived?.pageBg
491
+ ?? fallback.bg,
492
+ panel:
493
+ exported?.cardBg
494
+ ?? derived?.cardBg
495
+ ?? safeThemeColor(() => theme.getBgAnsi("toolPendingBg"))
496
+ ?? fallback.panel,
497
+ panel2:
498
+ derived?.panel2
499
+ ?? safeThemeColor(() => theme.getBgAnsi("selectedBg"))
500
+ ?? exported?.infoBg
501
+ ?? fallback.panel2,
304
502
  border: safeThemeColor(() => theme.getFgAnsi("border")) ?? fallback.border,
503
+ borderMuted: safeThemeColor(() => theme.getFgAnsi("borderMuted")) ?? fallback.borderMuted,
305
504
  text: safeThemeColor(() => theme.getFgAnsi("text")) ?? fallback.text,
306
505
  muted: safeThemeColor(() => theme.getFgAnsi("muted")) ?? fallback.muted,
307
506
  accent,
@@ -314,6 +513,25 @@ function getStudioThemeStyle(theme?: Theme): StudioThemeStyle {
314
513
  accentSoftStrong: withAlpha(accent, mode === "light" ? 0.35 : 0.40, fallback.accentSoftStrong),
315
514
  okBorder: withAlpha(ok, mode === "light" ? 0.55 : 0.70, fallback.okBorder),
316
515
  warnBorder: withAlpha(warn, mode === "light" ? 0.55 : 0.70, fallback.warnBorder),
516
+ mdHeading: safeThemeColor(() => theme.getFgAnsi("mdHeading")) ?? fallback.mdHeading,
517
+ mdLink: safeThemeColor(() => theme.getFgAnsi("mdLink")) ?? fallback.mdLink,
518
+ mdLinkUrl: safeThemeColor(() => theme.getFgAnsi("mdLinkUrl")) ?? fallback.mdLinkUrl,
519
+ mdCode: safeThemeColor(() => theme.getFgAnsi("mdCode")) ?? fallback.mdCode,
520
+ mdCodeBlock: safeThemeColor(() => theme.getFgAnsi("mdCodeBlock")) ?? fallback.mdCodeBlock,
521
+ mdCodeBlockBorder: safeThemeColor(() => theme.getFgAnsi("mdCodeBlockBorder")) ?? fallback.mdCodeBlockBorder,
522
+ mdQuote: safeThemeColor(() => theme.getFgAnsi("mdQuote")) ?? fallback.mdQuote,
523
+ mdQuoteBorder: safeThemeColor(() => theme.getFgAnsi("mdQuoteBorder")) ?? fallback.mdQuoteBorder,
524
+ mdHr: safeThemeColor(() => theme.getFgAnsi("mdHr")) ?? fallback.mdHr,
525
+ mdListBullet: safeThemeColor(() => theme.getFgAnsi("mdListBullet")) ?? fallback.mdListBullet,
526
+ syntaxComment: safeThemeColor(() => theme.getFgAnsi("syntaxComment")) ?? fallback.syntaxComment,
527
+ syntaxKeyword: safeThemeColor(() => theme.getFgAnsi("syntaxKeyword")) ?? fallback.syntaxKeyword,
528
+ syntaxFunction: safeThemeColor(() => theme.getFgAnsi("syntaxFunction")) ?? fallback.syntaxFunction,
529
+ syntaxVariable: safeThemeColor(() => theme.getFgAnsi("syntaxVariable")) ?? fallback.syntaxVariable,
530
+ syntaxString: safeThemeColor(() => theme.getFgAnsi("syntaxString")) ?? fallback.syntaxString,
531
+ syntaxNumber: safeThemeColor(() => theme.getFgAnsi("syntaxNumber")) ?? fallback.syntaxNumber,
532
+ syntaxType: safeThemeColor(() => theme.getFgAnsi("syntaxType")) ?? fallback.syntaxType,
533
+ syntaxOperator: safeThemeColor(() => theme.getFgAnsi("syntaxOperator")) ?? fallback.syntaxOperator,
534
+ syntaxPunctuation: safeThemeColor(() => theme.getFgAnsi("syntaxPunctuation")) ?? fallback.syntaxPunctuation,
317
535
  };
318
536
 
319
537
  return { mode, palette };
@@ -912,6 +1130,51 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
912
1130
  const initialLabel = escapeHtmlForInline(initialDocument?.label ?? "blank");
913
1131
  const initialPath = escapeHtmlForInline(initialDocument?.path ?? "");
914
1132
  const style = getStudioThemeStyle(theme);
1133
+ const mermaidConfig = {
1134
+ startOnLoad: false,
1135
+ theme: "base",
1136
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
1137
+ flowchart: {
1138
+ curve: "basis",
1139
+ },
1140
+ themeVariables: {
1141
+ background: style.palette.bg,
1142
+ primaryColor: style.palette.panel2,
1143
+ primaryTextColor: style.palette.text,
1144
+ primaryBorderColor: style.palette.mdCodeBlockBorder,
1145
+ secondaryColor: style.palette.panel,
1146
+ secondaryTextColor: style.palette.text,
1147
+ secondaryBorderColor: style.palette.mdCodeBlockBorder,
1148
+ tertiaryColor: style.palette.panel,
1149
+ tertiaryTextColor: style.palette.text,
1150
+ tertiaryBorderColor: style.palette.mdCodeBlockBorder,
1151
+ lineColor: style.palette.mdQuote,
1152
+ textColor: style.palette.text,
1153
+ edgeLabelBackground: style.palette.panel2,
1154
+ nodeBorder: style.palette.mdCodeBlockBorder,
1155
+ clusterBkg: style.palette.panel,
1156
+ clusterBorder: style.palette.mdCodeBlockBorder,
1157
+ titleColor: style.palette.mdHeading,
1158
+ },
1159
+ };
1160
+ const panelShadow =
1161
+ style.mode === "light"
1162
+ ? "0 1px 2px rgba(15, 23, 42, 0.03), 0 4px 14px rgba(15, 23, 42, 0.04)"
1163
+ : "0 1px 2px rgba(0, 0, 0, 0.36), 0 6px 18px rgba(0, 0, 0, 0.22)";
1164
+ const accentContrast = style.mode === "light" ? "#ffffff" : "#0e1616";
1165
+ const blockquoteBg = withAlpha(
1166
+ style.palette.mdQuoteBorder,
1167
+ style.mode === "light" ? 0.10 : 0.16,
1168
+ style.mode === "light" ? "rgba(15, 23, 42, 0.04)" : "rgba(255, 255, 255, 0.05)",
1169
+ );
1170
+ const tableAltBg = withAlpha(
1171
+ style.palette.mdCodeBlockBorder,
1172
+ style.mode === "light" ? 0.10 : 0.14,
1173
+ style.mode === "light" ? "rgba(15, 23, 42, 0.03)" : "rgba(255, 255, 255, 0.04)",
1174
+ );
1175
+ const editorBg = style.mode === "light"
1176
+ ? blendColors(style.palette.panel, "#ffffff", 0.5)
1177
+ : style.palette.panel;
915
1178
 
916
1179
  return `<!doctype html>
917
1180
  <html>
@@ -926,6 +1189,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
926
1189
  --panel: ${style.palette.panel};
927
1190
  --panel-2: ${style.palette.panel2};
928
1191
  --border: ${style.palette.border};
1192
+ --border-muted: ${style.palette.borderMuted};
929
1193
  --text: ${style.palette.text};
930
1194
  --muted: ${style.palette.muted};
931
1195
  --accent: ${style.palette.accent};
@@ -938,6 +1202,30 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
938
1202
  --accent-soft-strong: ${style.palette.accentSoftStrong};
939
1203
  --ok-border: ${style.palette.okBorder};
940
1204
  --warn-border: ${style.palette.warnBorder};
1205
+ --md-heading: ${style.palette.mdHeading};
1206
+ --md-link: ${style.palette.mdLink};
1207
+ --md-link-url: ${style.palette.mdLinkUrl};
1208
+ --md-code: ${style.palette.mdCode};
1209
+ --md-codeblock: ${style.palette.mdCodeBlock};
1210
+ --md-codeblock-border: ${style.palette.mdCodeBlockBorder};
1211
+ --md-quote: ${style.palette.mdQuote};
1212
+ --md-quote-border: ${style.palette.mdQuoteBorder};
1213
+ --md-hr: ${style.palette.mdHr};
1214
+ --md-list-bullet: ${style.palette.mdListBullet};
1215
+ --syntax-comment: ${style.palette.syntaxComment};
1216
+ --syntax-keyword: ${style.palette.syntaxKeyword};
1217
+ --syntax-function: ${style.palette.syntaxFunction};
1218
+ --syntax-variable: ${style.palette.syntaxVariable};
1219
+ --syntax-string: ${style.palette.syntaxString};
1220
+ --syntax-number: ${style.palette.syntaxNumber};
1221
+ --syntax-type: ${style.palette.syntaxType};
1222
+ --syntax-operator: ${style.palette.syntaxOperator};
1223
+ --syntax-punctuation: ${style.palette.syntaxPunctuation};
1224
+ --panel-shadow: ${panelShadow};
1225
+ --accent-contrast: ${accentContrast};
1226
+ --blockquote-bg: ${blockquoteBg};
1227
+ --table-alt-bg: ${tableAltBg};
1228
+ --editor-bg: ${editorBg};
941
1229
  }
942
1230
 
943
1231
  * { box-sizing: border-box; }
@@ -956,7 +1244,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
956
1244
  }
957
1245
 
958
1246
  header {
959
- border-bottom: 1px solid var(--border);
1247
+ border-bottom: 1px solid var(--border-muted);
960
1248
  padding: 12px 16px;
961
1249
  background: var(--panel);
962
1250
  display: flex;
@@ -997,23 +1285,50 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
997
1285
  }
998
1286
 
999
1287
  button, select, .file-label {
1000
- border: 1px solid var(--border);
1001
- background: var(--panel-2);
1288
+ border: 1px solid var(--border-muted);
1289
+ background: var(--panel);
1002
1290
  color: var(--text);
1003
1291
  border-radius: 8px;
1004
1292
  padding: 8px 10px;
1005
1293
  font-size: 13px;
1294
+ transition: background-color 120ms ease, border-color 120ms ease;
1006
1295
  }
1007
1296
 
1008
1297
  button {
1009
1298
  cursor: pointer;
1010
1299
  }
1011
1300
 
1301
+ button:not(:disabled):hover,
1302
+ select:hover,
1303
+ .file-label:hover {
1304
+ background: var(--panel-2);
1305
+ }
1306
+
1307
+ button:focus-visible,
1308
+ select:focus-visible,
1309
+ .file-label:focus-within {
1310
+ outline: 2px solid var(--accent-soft-strong);
1311
+ outline-offset: 1px;
1312
+ }
1313
+
1012
1314
  button:disabled {
1013
1315
  opacity: 0.6;
1014
1316
  cursor: not-allowed;
1015
1317
  }
1016
1318
 
1319
+ #sendRunBtn,
1320
+ #loadResponseBtn:not(:disabled):not([hidden]) {
1321
+ background: var(--accent);
1322
+ border-color: var(--accent);
1323
+ color: var(--accent-contrast);
1324
+ font-weight: 600;
1325
+ }
1326
+
1327
+ #sendRunBtn:not(:disabled):hover,
1328
+ #loadResponseBtn:not(:disabled):not([hidden]):hover {
1329
+ filter: brightness(0.95);
1330
+ }
1331
+
1017
1332
  .file-label {
1018
1333
  cursor: pointer;
1019
1334
  display: inline-flex;
@@ -1035,18 +1350,18 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1035
1350
  }
1036
1351
 
1037
1352
  section {
1038
- border: 1px solid var(--border);
1353
+ border: 1px solid var(--border-muted);
1039
1354
  border-radius: 10px;
1040
1355
  background: var(--panel);
1041
1356
  min-height: 0;
1042
1357
  display: flex;
1043
1358
  flex-direction: column;
1044
1359
  overflow: hidden;
1360
+ box-shadow: var(--panel-shadow);
1045
1361
  }
1046
1362
 
1047
1363
  section.pane-active {
1048
- border-color: var(--accent);
1049
- box-shadow: inset 0 0 0 1px var(--accent-soft);
1364
+ border-color: var(--border);
1050
1365
  }
1051
1366
 
1052
1367
  body.pane-focus-left main,
@@ -1061,28 +1376,28 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1061
1376
 
1062
1377
  body.pane-focus-left #leftPane,
1063
1378
  body.pane-focus-right #rightPane {
1064
- border-color: var(--accent);
1065
- box-shadow: inset 0 0 0 1px var(--accent-soft-strong);
1379
+ border-color: var(--border);
1066
1380
  }
1067
1381
 
1068
1382
  .section-header {
1069
1383
  padding: 10px 12px;
1070
- border-bottom: 1px solid var(--border);
1384
+ border-bottom: 1px solid var(--border-muted);
1385
+ background: var(--panel-2);
1071
1386
  font-weight: 600;
1072
1387
  font-size: 14px;
1073
1388
  }
1074
1389
 
1075
1390
  .reference-meta {
1076
1391
  padding: 8px 10px;
1077
- border-bottom: 1px solid var(--border);
1078
- background: var(--panel);
1392
+ border-bottom: 1px solid var(--border-muted);
1393
+ background: var(--panel-2);
1079
1394
  }
1080
1395
 
1081
1396
  textarea {
1082
1397
  width: 100%;
1083
- border: 1px solid var(--border);
1398
+ border: 1px solid var(--border-muted);
1084
1399
  border-radius: 8px;
1085
- background: var(--panel-2);
1400
+ background: var(--panel);
1086
1401
  color: var(--text);
1087
1402
  padding: 10px;
1088
1403
  font-size: 13px;
@@ -1093,7 +1408,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1093
1408
 
1094
1409
  .source-wrap {
1095
1410
  padding: 10px;
1096
- border-bottom: 1px solid var(--border);
1411
+ border-bottom: 1px solid var(--border-muted);
1097
1412
  display: flex;
1098
1413
  flex-direction: column;
1099
1414
  gap: 8px;
@@ -1117,8 +1432,8 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1117
1432
  }
1118
1433
 
1119
1434
  .source-badge {
1120
- border: 1px solid var(--border);
1121
- background: var(--panel-2);
1435
+ border: 1px solid var(--border-muted);
1436
+ background: var(--panel);
1122
1437
  border-radius: 999px;
1123
1438
  padding: 4px 10px;
1124
1439
  font-size: 12px;
@@ -1155,9 +1470,9 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1155
1470
  flex: 1 1 auto;
1156
1471
  min-height: 0;
1157
1472
  max-height: none;
1158
- border: 1px solid var(--border);
1473
+ border: 1px solid var(--border-muted);
1159
1474
  border-radius: 8px;
1160
- background: var(--panel-2);
1475
+ background: var(--editor-bg);
1161
1476
  overflow: hidden;
1162
1477
  }
1163
1478
 
@@ -1189,6 +1504,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1189
1504
  border-radius: 0;
1190
1505
  background: transparent;
1191
1506
  resize: none;
1507
+ outline: none;
1192
1508
  }
1193
1509
 
1194
1510
  #sourceText.highlight-active {
@@ -1205,7 +1521,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1205
1521
  }
1206
1522
 
1207
1523
  .hl-heading {
1208
- color: var(--accent);
1524
+ color: var(--md-heading);
1209
1525
  font-weight: 700;
1210
1526
  }
1211
1527
 
@@ -1214,58 +1530,58 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1214
1530
  }
1215
1531
 
1216
1532
  .hl-code {
1217
- color: var(--ok);
1533
+ color: var(--md-code);
1218
1534
  }
1219
1535
 
1220
1536
  .hl-code-kw {
1221
- color: var(--accent);
1537
+ color: var(--syntax-keyword);
1222
1538
  font-weight: 600;
1223
1539
  }
1224
1540
 
1225
1541
  .hl-code-str {
1226
- color: var(--ok);
1542
+ color: var(--syntax-string);
1227
1543
  }
1228
1544
 
1229
1545
  .hl-code-num {
1230
- color: var(--warn);
1546
+ color: var(--syntax-number);
1231
1547
  }
1232
1548
 
1233
1549
  .hl-code-com {
1234
- color: var(--muted);
1550
+ color: var(--syntax-comment);
1235
1551
  font-style: italic;
1236
1552
  }
1237
1553
 
1238
1554
  .hl-code-var,
1239
1555
  .hl-code-key {
1240
- color: var(--accent);
1556
+ color: var(--syntax-variable);
1241
1557
  }
1242
1558
 
1243
1559
  .hl-list {
1244
- color: var(--accent);
1560
+ color: var(--md-list-bullet);
1245
1561
  font-weight: 600;
1246
1562
  }
1247
1563
 
1248
1564
  .hl-quote {
1249
- color: var(--muted);
1565
+ color: var(--md-quote);
1250
1566
  font-style: italic;
1251
1567
  }
1252
1568
 
1253
1569
  .hl-link {
1254
- color: var(--accent);
1570
+ color: var(--md-link);
1255
1571
  text-decoration: underline;
1256
1572
  }
1257
1573
 
1258
1574
  .hl-url {
1259
- color: var(--muted);
1575
+ color: var(--md-link-url);
1260
1576
  }
1261
1577
 
1262
1578
  #sourcePreview {
1263
1579
  flex: 1 1 auto;
1264
1580
  min-height: 0;
1265
1581
  max-height: none;
1266
- border: 1px solid var(--border);
1582
+ border: 1px solid var(--border-muted);
1267
1583
  border-radius: 8px;
1268
- background: var(--panel-2);
1584
+ background: var(--panel);
1269
1585
  }
1270
1586
 
1271
1587
  .panel-scroll {
@@ -1291,18 +1607,20 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1291
1607
  margin-top: 1.2em;
1292
1608
  margin-bottom: 0.5em;
1293
1609
  line-height: 1.25;
1610
+ letter-spacing: -0.01em;
1611
+ color: var(--md-heading);
1294
1612
  }
1295
1613
 
1296
1614
  .rendered-markdown h1 {
1297
- font-size: 2em;
1298
- border-bottom: 1px solid var(--border);
1299
- padding-bottom: 0.3em;
1615
+ font-size: 1.6em;
1616
+ border-bottom: 0;
1617
+ padding-bottom: 0;
1300
1618
  }
1301
1619
 
1302
1620
  .rendered-markdown h2 {
1303
- font-size: 1.5em;
1304
- border-bottom: 1px solid var(--border);
1305
- padding-bottom: 0.25em;
1621
+ font-size: 1.25em;
1622
+ border-bottom: 0;
1623
+ padding-bottom: 0;
1306
1624
  }
1307
1625
 
1308
1626
  .rendered-markdown p,
@@ -1314,8 +1632,12 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1314
1632
  margin-bottom: 1em;
1315
1633
  }
1316
1634
 
1635
+ .rendered-markdown li::marker {
1636
+ color: var(--md-list-bullet);
1637
+ }
1638
+
1317
1639
  .rendered-markdown a {
1318
- color: var(--accent);
1640
+ color: var(--md-link);
1319
1641
  text-decoration: none;
1320
1642
  }
1321
1643
 
@@ -1323,16 +1645,23 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1323
1645
  text-decoration: underline;
1324
1646
  }
1325
1647
 
1648
+ .rendered-markdown a.uri,
1649
+ .rendered-markdown .uri {
1650
+ color: var(--md-link-url);
1651
+ }
1652
+
1326
1653
  .rendered-markdown blockquote {
1327
1654
  margin-left: 0;
1328
- padding: 0 1em;
1329
- border-left: 0.25em solid var(--border);
1330
- color: var(--muted);
1655
+ padding: 0.2em 1em;
1656
+ border-left: 0.25em solid var(--md-quote-border);
1657
+ border-radius: 0 8px 8px 0;
1658
+ background: var(--blockquote-bg);
1659
+ color: var(--md-quote);
1331
1660
  }
1332
1661
 
1333
1662
  .rendered-markdown pre {
1334
- background: var(--panel);
1335
- border: 1px solid var(--border);
1663
+ background: var(--panel-2);
1664
+ border: 1px solid var(--md-codeblock-border);
1336
1665
  border-radius: 8px;
1337
1666
  padding: 12px 14px;
1338
1667
  overflow: auto;
@@ -1343,49 +1672,67 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1343
1672
  .rendered-markdown code {
1344
1673
  font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
1345
1674
  font-size: 0.9em;
1675
+ color: var(--md-code);
1676
+ }
1677
+
1678
+ .rendered-markdown pre code {
1679
+ color: var(--md-codeblock);
1346
1680
  }
1347
1681
 
1348
1682
  .rendered-markdown :not(pre) > code {
1349
- background: rgba(127, 127, 127, 0.16);
1350
- border: 1px solid var(--border);
1683
+ background: rgba(127, 127, 127, 0.13);
1684
+ border: 1px solid var(--md-codeblock-border);
1351
1685
  border-radius: 6px;
1352
1686
  padding: 0.12em 0.35em;
1353
1687
  }
1354
1688
 
1355
1689
  .rendered-markdown code span.kw,
1356
1690
  .rendered-markdown code span.cf,
1357
- .rendered-markdown code span.im,
1691
+ .rendered-markdown code span.im {
1692
+ color: var(--syntax-keyword);
1693
+ font-weight: 600;
1694
+ }
1695
+
1358
1696
  .rendered-markdown code span.dt {
1359
- color: var(--accent);
1697
+ color: var(--syntax-type);
1360
1698
  font-weight: 600;
1361
1699
  }
1362
1700
 
1363
1701
  .rendered-markdown code span.fu,
1364
- .rendered-markdown code span.bu,
1702
+ .rendered-markdown code span.bu {
1703
+ color: var(--syntax-function);
1704
+ }
1705
+
1365
1706
  .rendered-markdown code span.va,
1366
1707
  .rendered-markdown code span.ot {
1367
- color: var(--accent);
1708
+ color: var(--syntax-variable);
1368
1709
  }
1369
1710
 
1370
1711
  .rendered-markdown code span.st,
1371
1712
  .rendered-markdown code span.ss,
1372
- .rendered-markdown code span.sc {
1373
- color: var(--ok);
1713
+ .rendered-markdown code span.sc,
1714
+ .rendered-markdown code span.ch {
1715
+ color: var(--syntax-string);
1374
1716
  }
1375
1717
 
1376
1718
  .rendered-markdown code span.dv,
1377
1719
  .rendered-markdown code span.bn,
1378
1720
  .rendered-markdown code span.fl {
1379
- color: var(--warn);
1721
+ color: var(--syntax-number);
1380
1722
  }
1381
1723
 
1382
1724
  .rendered-markdown code span.co {
1383
- color: var(--muted);
1725
+ color: var(--syntax-comment);
1384
1726
  font-style: italic;
1385
1727
  }
1386
1728
 
1387
1729
  .rendered-markdown code span.op {
1388
- color: var(--text);
1730
+ color: var(--syntax-operator);
1731
+ }
1732
+
1733
+ .rendered-markdown code span.pp,
1734
+ .rendered-markdown code span.pu {
1735
+ color: var(--syntax-punctuation);
1389
1736
  }
1390
1737
 
1391
1738
  .rendered-markdown code span.er,
@@ -1403,13 +1750,21 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1403
1750
 
1404
1751
  .rendered-markdown th,
1405
1752
  .rendered-markdown td {
1406
- border: 1px solid var(--border);
1753
+ border: 1px solid var(--border-muted);
1407
1754
  padding: 6px 12px;
1408
1755
  }
1409
1756
 
1757
+ .rendered-markdown thead th {
1758
+ background: var(--panel-2);
1759
+ }
1760
+
1761
+ .rendered-markdown tbody tr:nth-child(even) {
1762
+ background: var(--table-alt-bg);
1763
+ }
1764
+
1410
1765
  .rendered-markdown hr {
1411
1766
  border: 0;
1412
- border-top: 1px solid var(--border);
1767
+ border-top: 1px solid var(--md-hr);
1413
1768
  margin: 1.25em 0;
1414
1769
  }
1415
1770
 
@@ -1424,6 +1779,17 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1424
1779
  overflow-y: hidden;
1425
1780
  }
1426
1781
 
1782
+ .rendered-markdown .mermaid-container {
1783
+ text-align: center;
1784
+ margin: 1em 0;
1785
+ overflow-x: auto;
1786
+ }
1787
+
1788
+ .rendered-markdown .mermaid-container svg {
1789
+ max-width: 100%;
1790
+ height: auto;
1791
+ }
1792
+
1427
1793
  .plain-markdown {
1428
1794
  margin: 0;
1429
1795
  white-space: pre-wrap;
@@ -1453,6 +1819,13 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1453
1819
  font-size: 12px;
1454
1820
  }
1455
1821
 
1822
+ .preview-warning {
1823
+ color: var(--warn);
1824
+ margin-top: 0.75em;
1825
+ font-size: 12px;
1826
+ font-style: italic;
1827
+ }
1828
+
1456
1829
  .marker {
1457
1830
  display: inline-block;
1458
1831
  padding: 0 4px;
@@ -1478,7 +1851,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1478
1851
  }
1479
1852
 
1480
1853
  .response-wrap {
1481
- border-top: 1px solid var(--border);
1854
+ border-top: 1px solid var(--border-muted);
1482
1855
  padding: 10px;
1483
1856
  display: flex;
1484
1857
  flex-direction: column;
@@ -1494,7 +1867,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1494
1867
  }
1495
1868
 
1496
1869
  footer {
1497
- border-top: 1px solid var(--border);
1870
+ border-top: 1px solid var(--border-muted);
1498
1871
  padding: 8px 12px;
1499
1872
  color: var(--muted);
1500
1873
  font-size: 12px;
@@ -1535,11 +1908,11 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1535
1908
  <h1><span class="app-logo" aria-hidden="true">π</span> Pi Studio <span class="app-subtitle">Feedback Workspace</span></h1>
1536
1909
  <div class="controls">
1537
1910
  <select id="editorViewSelect" aria-label="Editor view mode">
1538
- <option value="markdown" selected>Editor: Markdown</option>
1911
+ <option value="markdown" selected>Editor: Raw</option>
1539
1912
  <option value="preview">Editor: Preview</option>
1540
1913
  </select>
1541
1914
  <select id="rightViewSelect" aria-label="Response view mode">
1542
- <option value="markdown">Response: Markdown</option>
1915
+ <option value="markdown">Response: Raw</option>
1543
1916
  <option value="preview" selected>Response: Preview</option>
1544
1917
  </select>
1545
1918
  <button id="saveAsBtn" type="button" title="Save editor text to a new file path.">Save As…</button>
@@ -1569,8 +1942,8 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1569
1942
  <button id="sendEditorBtn" type="button">Send to pi editor</button>
1570
1943
  <button id="copyDraftBtn" type="button">Copy editor text</button>
1571
1944
  <select id="highlightSelect" aria-label="Editor syntax highlighting">
1572
- <option value="off">Highlight markdown: Off</option>
1573
- <option value="on" selected>Highlight markdown: On</option>
1945
+ <option value="off">Syntax highlight: Off</option>
1946
+ <option value="on" selected>Syntax highlight: On</option>
1574
1947
  </select>
1575
1948
  </div>
1576
1949
  </div>
@@ -1595,8 +1968,8 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1595
1968
  <option value="off">Auto-update response: Off</option>
1596
1969
  </select>
1597
1970
  <select id="responseHighlightSelect" aria-label="Response markdown highlighting">
1598
- <option value="off">Highlight markdown: Off</option>
1599
- <option value="on" selected>Highlight markdown: On</option>
1971
+ <option value="off">Syntax highlight: Off</option>
1972
+ <option value="on" selected>Syntax highlight: On</option>
1600
1973
  </select>
1601
1974
  <button id="pullLatestBtn" type="button" title="Fetch the latest assistant response when auto-update is off.">Get latest response</button>
1602
1975
  <button id="loadResponseBtn" type="button">Load response into editor</button>
@@ -1710,6 +2083,12 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1710
2083
  let editorHighlightEnabled = false;
1711
2084
  let responseHighlightEnabled = false;
1712
2085
  let editorHighlightRenderRaf = null;
2086
+ const MERMAID_CDN_URL = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
2087
+ const MERMAID_CONFIG = ${JSON.stringify(mermaidConfig)};
2088
+ const MERMAID_UNAVAILABLE_MESSAGE = "Mermaid renderer unavailable. Showing mermaid blocks as code.";
2089
+ const MERMAID_RENDER_FAIL_MESSAGE = "Mermaid render failed. Showing diagram source text.";
2090
+ let mermaidModulePromise = null;
2091
+ let mermaidInitialized = false;
1713
2092
 
1714
2093
  function getIdleStatus() {
1715
2094
  return "Ready. Edit text, then run or critique (insert annotation header if needed).";
@@ -1813,6 +2192,19 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1813
2192
  event.preventDefault();
1814
2193
  }
1815
2194
  }
2195
+
2196
+ if (
2197
+ key === "Enter"
2198
+ && (event.metaKey || event.ctrlKey)
2199
+ && !event.altKey
2200
+ && !event.shiftKey
2201
+ && activePane === "left"
2202
+ && sendRunBtn
2203
+ && !sendRunBtn.disabled
2204
+ ) {
2205
+ event.preventDefault();
2206
+ sendRunBtn.click();
2207
+ }
1816
2208
  }
1817
2209
 
1818
2210
  function formatReferenceTime(timestamp) {
@@ -1906,6 +2298,93 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1906
2298
  return buildPreviewErrorHtml("Preview sanitizer unavailable. Showing plain markdown.", markdown);
1907
2299
  }
1908
2300
 
2301
+ function appendMermaidNotice(targetEl, message) {
2302
+ if (!targetEl || typeof targetEl.querySelector !== "function" || typeof targetEl.appendChild !== "function") {
2303
+ return;
2304
+ }
2305
+
2306
+ if (targetEl.querySelector(".preview-mermaid-warning")) {
2307
+ return;
2308
+ }
2309
+
2310
+ const warningEl = document.createElement("div");
2311
+ warningEl.className = "preview-warning preview-mermaid-warning";
2312
+ warningEl.textContent = String(message || MERMAID_RENDER_FAIL_MESSAGE);
2313
+ targetEl.appendChild(warningEl);
2314
+ }
2315
+
2316
+ async function getMermaidApi() {
2317
+ if (mermaidModulePromise) {
2318
+ return mermaidModulePromise;
2319
+ }
2320
+
2321
+ mermaidModulePromise = import(MERMAID_CDN_URL)
2322
+ .then((module) => {
2323
+ const mermaidApi = module && module.default ? module.default : null;
2324
+ if (!mermaidApi) {
2325
+ throw new Error("Mermaid module did not expose a default export.");
2326
+ }
2327
+
2328
+ if (!mermaidInitialized) {
2329
+ mermaidApi.initialize(MERMAID_CONFIG);
2330
+ mermaidInitialized = true;
2331
+ }
2332
+
2333
+ return mermaidApi;
2334
+ })
2335
+ .catch((error) => {
2336
+ mermaidModulePromise = null;
2337
+ throw error;
2338
+ });
2339
+
2340
+ return mermaidModulePromise;
2341
+ }
2342
+
2343
+ async function renderMermaidInElement(targetEl) {
2344
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
2345
+
2346
+ const mermaidBlocks = targetEl.querySelectorAll("pre.mermaid");
2347
+ if (!mermaidBlocks || mermaidBlocks.length === 0) return;
2348
+
2349
+ let mermaidApi;
2350
+ try {
2351
+ mermaidApi = await getMermaidApi();
2352
+ } catch (error) {
2353
+ console.error("Mermaid module load failed:", error);
2354
+ appendMermaidNotice(targetEl, MERMAID_UNAVAILABLE_MESSAGE);
2355
+ return;
2356
+ }
2357
+
2358
+ mermaidBlocks.forEach((preEl) => {
2359
+ const codeEl = preEl.querySelector("code");
2360
+ const source = codeEl ? codeEl.textContent : preEl.textContent;
2361
+
2362
+ const wrapper = document.createElement("div");
2363
+ wrapper.className = "mermaid-container";
2364
+
2365
+ const diagramEl = document.createElement("div");
2366
+ diagramEl.className = "mermaid";
2367
+ diagramEl.textContent = source || "";
2368
+
2369
+ wrapper.appendChild(diagramEl);
2370
+ preEl.replaceWith(wrapper);
2371
+ });
2372
+
2373
+ const diagramNodes = Array.from(targetEl.querySelectorAll(".mermaid"));
2374
+ if (diagramNodes.length === 0) return;
2375
+
2376
+ try {
2377
+ await mermaidApi.run({ nodes: diagramNodes });
2378
+ } catch (error) {
2379
+ try {
2380
+ await mermaidApi.run();
2381
+ } catch (fallbackError) {
2382
+ console.error("Mermaid render failed:", fallbackError || error);
2383
+ appendMermaidNotice(targetEl, MERMAID_RENDER_FAIL_MESSAGE);
2384
+ }
2385
+ }
2386
+ }
2387
+
1909
2388
  async function renderMarkdownWithPandoc(markdown) {
1910
2389
  const token = getToken();
1911
2390
  if (!token) {
@@ -1976,6 +2455,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1976
2455
  }
1977
2456
 
1978
2457
  targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
2458
+ await renderMermaidInElement(targetEl);
1979
2459
  } catch (error) {
1980
2460
  if (pane === "source") {
1981
2461
  if (nonce !== sourcePreviewRenderNonce || editorView !== "preview") return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.1.8",
3
+ "version": "0.2.0",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",