pi-studio 0.1.7 → 0.1.9

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
@@ -25,6 +25,7 @@ All notable changes to `pi-studio` are documented here.
25
25
  - Response-side markdown highlighting toggle (`Highlight markdown: Off|On`) in `Response: Markdown` view, with local preference persistence.
26
26
  - Markdown highlighter now applies lightweight fenced-code token colors for common languages (`js/ts`, `python`, `bash/sh`, `json`).
27
27
  - Obsidian wiki-image syntax normalization (`![[path]]`, `![[path|alt]]`) before pandoc preview rendering.
28
+ - Client-side Mermaid rendering for fenced `mermaid` code blocks in both Preview panes.
28
29
 
29
30
  ### Changed
30
31
  - Removed Annotate/Critique tabs and related mode state.
@@ -47,6 +48,7 @@ All notable changes to `pi-studio` are documented here.
47
48
  - `respondText` now includes `X-Content-Type-Options: nosniff` for consistency with JSON responses.
48
49
  - If `dompurify` is unavailable, preview now falls back to escaped plain markdown instead of injecting unsanitized HTML.
49
50
  - Preview sanitization now preserves MathML profile and strips MathML annotation tags to avoid duplicate raw TeX text beside rendered equations.
51
+ - Preview now shows an inline warning when Mermaid is unavailable or diagram rendering fails, instead of failing silently.
50
52
 
51
53
  ### Changed
52
54
  - Added npm metadata fields (`repository`, `homepage`, `bugs`) so npm package page links to GitHub.
package/README.md CHANGED
@@ -33,6 +33,7 @@ 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
38
  - Theme-aware browser UI based on current pi theme
38
39
 
@@ -75,6 +76,8 @@ 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
+ - Mermaid fenced `mermaid` code blocks are rendered client-side in preview mode (Mermaid v11 loaded from jsDelivr).
80
+ - If Mermaid cannot load or a diagram fails to render, preview shows an inline warning and keeps source text visible.
78
81
  - Preview rendering normalizes Obsidian wiki-image syntax (`![[path]]`, `![[path|alt]]`) into standard markdown images.
79
82
  - Install pandoc for full preview rendering (`brew install pandoc` on macOS).
80
83
  - If `pandoc` is unavailable, preview falls back to plain markdown text with an inline warning.
package/index.ts CHANGED
@@ -1424,6 +1424,17 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1424
1424
  overflow-y: hidden;
1425
1425
  }
1426
1426
 
1427
+ .rendered-markdown .mermaid-container {
1428
+ text-align: center;
1429
+ margin: 1em 0;
1430
+ overflow-x: auto;
1431
+ }
1432
+
1433
+ .rendered-markdown .mermaid-container svg {
1434
+ max-width: 100%;
1435
+ height: auto;
1436
+ }
1437
+
1427
1438
  .plain-markdown {
1428
1439
  margin: 0;
1429
1440
  white-space: pre-wrap;
@@ -1453,6 +1464,13 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1453
1464
  font-size: 12px;
1454
1465
  }
1455
1466
 
1467
+ .preview-warning {
1468
+ color: var(--warn);
1469
+ margin-top: 0.75em;
1470
+ font-size: 12px;
1471
+ font-style: italic;
1472
+ }
1473
+
1456
1474
  .marker {
1457
1475
  display: inline-block;
1458
1476
  padding: 0 4px;
@@ -1569,8 +1587,8 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1569
1587
  <button id="sendEditorBtn" type="button">Send to pi editor</button>
1570
1588
  <button id="copyDraftBtn" type="button">Copy editor text</button>
1571
1589
  <select id="highlightSelect" aria-label="Editor syntax highlighting">
1572
- <option value="off" selected>Highlight markdown: Off</option>
1573
- <option value="on">Highlight markdown: On</option>
1590
+ <option value="off">Highlight markdown: Off</option>
1591
+ <option value="on" selected>Highlight markdown: On</option>
1574
1592
  </select>
1575
1593
  </div>
1576
1594
  </div>
@@ -1595,8 +1613,8 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1595
1613
  <option value="off">Auto-update response: Off</option>
1596
1614
  </select>
1597
1615
  <select id="responseHighlightSelect" aria-label="Response markdown highlighting">
1598
- <option value="off" selected>Highlight markdown: Off</option>
1599
- <option value="on">Highlight markdown: On</option>
1616
+ <option value="off">Highlight markdown: Off</option>
1617
+ <option value="on" selected>Highlight markdown: On</option>
1600
1618
  </select>
1601
1619
  <button id="pullLatestBtn" type="button" title="Fetch the latest assistant response when auto-update is off.">Get latest response</button>
1602
1620
  <button id="loadResponseBtn" type="button">Load response into editor</button>
@@ -1710,6 +1728,12 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1710
1728
  let editorHighlightEnabled = false;
1711
1729
  let responseHighlightEnabled = false;
1712
1730
  let editorHighlightRenderRaf = null;
1731
+ const MERMAID_CDN_URL = "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
1732
+ const MERMAID_THEME = "${style.mode === "dark" ? "dark" : "default"}";
1733
+ const MERMAID_UNAVAILABLE_MESSAGE = "Mermaid renderer unavailable. Showing mermaid blocks as code.";
1734
+ const MERMAID_RENDER_FAIL_MESSAGE = "Mermaid render failed. Showing diagram source text.";
1735
+ let mermaidModulePromise = null;
1736
+ let mermaidInitialized = false;
1713
1737
 
1714
1738
  function getIdleStatus() {
1715
1739
  return "Ready. Edit text, then run or critique (insert annotation header if needed).";
@@ -1906,6 +1930,96 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1906
1930
  return buildPreviewErrorHtml("Preview sanitizer unavailable. Showing plain markdown.", markdown);
1907
1931
  }
1908
1932
 
1933
+ function appendMermaidNotice(targetEl, message) {
1934
+ if (!targetEl || typeof targetEl.querySelector !== "function" || typeof targetEl.appendChild !== "function") {
1935
+ return;
1936
+ }
1937
+
1938
+ if (targetEl.querySelector(".preview-mermaid-warning")) {
1939
+ return;
1940
+ }
1941
+
1942
+ const warningEl = document.createElement("div");
1943
+ warningEl.className = "preview-warning preview-mermaid-warning";
1944
+ warningEl.textContent = String(message || MERMAID_RENDER_FAIL_MESSAGE);
1945
+ targetEl.appendChild(warningEl);
1946
+ }
1947
+
1948
+ async function getMermaidApi() {
1949
+ if (mermaidModulePromise) {
1950
+ return mermaidModulePromise;
1951
+ }
1952
+
1953
+ mermaidModulePromise = import(MERMAID_CDN_URL)
1954
+ .then((module) => {
1955
+ const mermaidApi = module && module.default ? module.default : null;
1956
+ if (!mermaidApi) {
1957
+ throw new Error("Mermaid module did not expose a default export.");
1958
+ }
1959
+
1960
+ if (!mermaidInitialized) {
1961
+ mermaidApi.initialize({
1962
+ startOnLoad: false,
1963
+ theme: MERMAID_THEME,
1964
+ });
1965
+ mermaidInitialized = true;
1966
+ }
1967
+
1968
+ return mermaidApi;
1969
+ })
1970
+ .catch((error) => {
1971
+ mermaidModulePromise = null;
1972
+ throw error;
1973
+ });
1974
+
1975
+ return mermaidModulePromise;
1976
+ }
1977
+
1978
+ async function renderMermaidInElement(targetEl) {
1979
+ if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
1980
+
1981
+ const mermaidBlocks = targetEl.querySelectorAll("pre.mermaid");
1982
+ if (!mermaidBlocks || mermaidBlocks.length === 0) return;
1983
+
1984
+ let mermaidApi;
1985
+ try {
1986
+ mermaidApi = await getMermaidApi();
1987
+ } catch (error) {
1988
+ console.error("Mermaid module load failed:", error);
1989
+ appendMermaidNotice(targetEl, MERMAID_UNAVAILABLE_MESSAGE);
1990
+ return;
1991
+ }
1992
+
1993
+ mermaidBlocks.forEach((preEl) => {
1994
+ const codeEl = preEl.querySelector("code");
1995
+ const source = codeEl ? codeEl.textContent : preEl.textContent;
1996
+
1997
+ const wrapper = document.createElement("div");
1998
+ wrapper.className = "mermaid-container";
1999
+
2000
+ const diagramEl = document.createElement("div");
2001
+ diagramEl.className = "mermaid";
2002
+ diagramEl.textContent = source || "";
2003
+
2004
+ wrapper.appendChild(diagramEl);
2005
+ preEl.replaceWith(wrapper);
2006
+ });
2007
+
2008
+ const diagramNodes = Array.from(targetEl.querySelectorAll(".mermaid"));
2009
+ if (diagramNodes.length === 0) return;
2010
+
2011
+ try {
2012
+ await mermaidApi.run({ nodes: diagramNodes });
2013
+ } catch (error) {
2014
+ try {
2015
+ await mermaidApi.run();
2016
+ } catch (fallbackError) {
2017
+ console.error("Mermaid render failed:", fallbackError || error);
2018
+ appendMermaidNotice(targetEl, MERMAID_RENDER_FAIL_MESSAGE);
2019
+ }
2020
+ }
2021
+ }
2022
+
1909
2023
  async function renderMarkdownWithPandoc(markdown) {
1910
2024
  const token = getToken();
1911
2025
  if (!token) {
@@ -1976,6 +2090,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1976
2090
  }
1977
2091
 
1978
2092
  targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown);
2093
+ await renderMermaidInElement(targetEl);
1979
2094
  } catch (error) {
1980
2095
  if (pane === "source") {
1981
2096
  if (nonce !== sourcePreviewRenderNonce || editorView !== "preview") return;
@@ -2454,11 +2569,14 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2454
2569
  }
2455
2570
 
2456
2571
  function readStoredToggle(storageKey) {
2457
- if (!window.localStorage) return false;
2572
+ if (!window.localStorage) return null;
2458
2573
  try {
2459
- return window.localStorage.getItem(storageKey) === "on";
2574
+ const value = window.localStorage.getItem(storageKey);
2575
+ if (value === "on") return true;
2576
+ if (value === "off") return false;
2577
+ return null;
2460
2578
  } catch {
2461
- return false;
2579
+ return null;
2462
2580
  }
2463
2581
  }
2464
2582
 
@@ -3232,12 +3350,12 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
3232
3350
  refreshResponseUi();
3233
3351
  setActivePane("left");
3234
3352
 
3235
- const initialHighlightEnabled = readStoredEditorHighlightEnabled()
3236
- || Boolean(highlightSelect && highlightSelect.value === "on");
3353
+ const storedEditorHighlightEnabled = readStoredEditorHighlightEnabled();
3354
+ const initialHighlightEnabled = storedEditorHighlightEnabled ?? Boolean(highlightSelect && highlightSelect.value === "on");
3237
3355
  setEditorHighlightEnabled(initialHighlightEnabled);
3238
3356
 
3239
- const initialResponseHighlightEnabled = readStoredResponseHighlightEnabled()
3240
- || Boolean(responseHighlightSelect && responseHighlightSelect.value === "on");
3357
+ const storedResponseHighlightEnabled = readStoredResponseHighlightEnabled();
3358
+ const initialResponseHighlightEnabled = storedResponseHighlightEnabled ?? Boolean(responseHighlightSelect && responseHighlightSelect.value === "on");
3241
3359
  setResponseHighlightEnabled(initialResponseHighlightEnabled);
3242
3360
 
3243
3361
  setEditorView(editorView);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",