pi-studio 0.1.2 → 0.1.4

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
@@ -22,6 +22,8 @@ All notable changes to `pi-studio` are documented here.
22
22
  - Math delimiter normalization before preview rendering for `\(...\)` and `\[...\]` syntax (fence-aware).
23
23
  - **Load file in editor** action in top controls (browser file picker into editor).
24
24
  - README screenshot gallery for dark/light workspace and critique/annotation views.
25
+ - Response-side markdown highlighting toggle (`Highlight response: Off|On`) in `Response: Markdown` view, with local preference persistence.
26
+ - Obsidian wiki-image syntax normalization (`![[path]]`, `![[path|alt]]`) before pandoc preview rendering.
25
27
 
26
28
  ### Changed
27
29
  - Removed Annotate/Critique tabs and related mode state.
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 Over**, **Load file in editor**
35
35
  - View toggles: `Editor: Markdown|Preview`, `Response: Markdown|Preview`
36
+ - Optional markdown highlighting toggles for editor and response markdown views
36
37
  - Theme-aware browser UI based on current pi theme
37
38
 
38
39
  ## Commands
@@ -73,6 +74,7 @@ pi -e https://github.com/omaclaren/pi-studio
73
74
  - One studio request at a time.
74
75
  - Studio URLs include a token query parameter; avoid sharing full Studio URLs.
75
76
  - Preview panes render markdown via `pandoc` (`gfm+tex_math_dollars` → HTML5 + MathML), sanitized in-browser with `dompurify`.
77
+ - Preview rendering normalizes Obsidian wiki-image syntax (`![[path]]`, `![[path|alt]]`) into standard markdown images.
76
78
  - Install pandoc for full preview rendering (`brew install pandoc` on macOS).
77
79
  - If `pandoc` is unavailable, preview falls back to plain markdown text with an inline warning.
78
80
  - Some screenshots may show the Grammarly browser widget. Grammarly is a separate browser extension and is not part of pi-studio, but it works alongside it.
package/index.ts CHANGED
@@ -491,10 +491,16 @@ function stripMathMlAnnotationTags(html: string): string {
491
491
  .replace(/<annotation\b[\s\S]*?<\/annotation>/gi, "");
492
492
  }
493
493
 
494
+ function normalizeObsidianImages(markdown: string): string {
495
+ return markdown
496
+ .replace(/!\[\[([^|\]]+)\|([^\]]+)\]\]/g, "![$2]($1)")
497
+ .replace(/!\[\[([^\]]+)\]\]/g, "![]($1)");
498
+ }
499
+
494
500
  async function renderStudioMarkdownWithPandoc(markdown: string): Promise<string> {
495
501
  const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
496
502
  const args = ["-f", "gfm+tex_math_dollars-raw_html", "-t", "html5", "--mathml", "--no-highlight"];
497
- const normalizedMarkdown = normalizeMathDelimiters(markdown);
503
+ const normalizedMarkdown = normalizeObsidianImages(normalizeMathDelimiters(markdown));
498
504
 
499
505
  return await new Promise<string>((resolve, reject) => {
500
506
  const child = spawn(pandocCommand, args, { stdio: ["pipe", "pipe", "pipe"] });
@@ -1362,6 +1368,15 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1362
1368
  line-height: 1.5;
1363
1369
  }
1364
1370
 
1371
+ .response-markdown-highlight {
1372
+ margin: 0;
1373
+ white-space: pre-wrap;
1374
+ word-break: break-word;
1375
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
1376
+ font-size: 13px;
1377
+ line-height: 1.5;
1378
+ }
1379
+
1365
1380
  .preview-loading {
1366
1381
  color: var(--muted);
1367
1382
  font-style: italic;
@@ -1478,8 +1493,8 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1478
1493
  <span id="syncBadge" class="source-badge sync-badge">No response loaded</span>
1479
1494
  </div>
1480
1495
  <div class="source-actions">
1481
- <button id="insertHeaderBtn" type="button" title="Prepends/updates the annotated-reply header in the editor.">Insert annotation header</button>
1482
1496
  <button id="sendRunBtn" type="button" title="Send editor text directly to the model as-is.">Run editor text</button>
1497
+ <button id="insertHeaderBtn" type="button" title="Prepends/updates the annotated-reply header in the editor.">Insert annotation header</button>
1483
1498
  <select id="lensSelect" aria-label="Critique focus">
1484
1499
  <option value="auto" selected>Critique focus: Auto</option>
1485
1500
  <option value="writing">Critique focus: Writing</option>
@@ -1514,6 +1529,10 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1514
1529
  <option value="on" selected>Auto-update response: On</option>
1515
1530
  <option value="off">Auto-update response: Off</option>
1516
1531
  </select>
1532
+ <select id="responseHighlightSelect" aria-label="Response markdown highlighting">
1533
+ <option value="off" selected>Highlight response: Off</option>
1534
+ <option value="on">Highlight response: On</option>
1535
+ </select>
1517
1536
  <button id="pullLatestBtn" type="button" title="Fetch the latest assistant response when auto-update is off.">Get latest response</button>
1518
1537
  <button id="loadResponseBtn" type="button">Load response into editor</button>
1519
1538
  <button id="loadCritiqueNotesBtn" type="button" hidden>Load critique (notes)</button>
@@ -1570,6 +1589,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1570
1589
  const editorViewSelect = document.getElementById("editorViewSelect");
1571
1590
  const rightViewSelect = document.getElementById("rightViewSelect");
1572
1591
  const followSelect = document.getElementById("followSelect");
1592
+ const responseHighlightSelect = document.getElementById("responseHighlightSelect");
1573
1593
  const pullLatestBtn = document.getElementById("pullLatestBtn");
1574
1594
  const insertHeaderBtn = document.getElementById("insertHeaderBtn");
1575
1595
  const critiqueBtn = document.getElementById("critiqueBtn");
@@ -1617,10 +1637,13 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1617
1637
  let paneFocusTarget = "off";
1618
1638
  const EDITOR_HIGHLIGHT_MAX_CHARS = 80_000;
1619
1639
  const EDITOR_HIGHLIGHT_STORAGE_KEY = "piStudio.editorHighlightEnabled";
1640
+ const RESPONSE_HIGHLIGHT_MAX_CHARS = 120_000;
1641
+ const RESPONSE_HIGHLIGHT_STORAGE_KEY = "piStudio.responseHighlightEnabled";
1620
1642
  let sourcePreviewRenderTimer = null;
1621
1643
  let sourcePreviewRenderNonce = 0;
1622
1644
  let responsePreviewRenderNonce = 0;
1623
1645
  let editorHighlightEnabled = false;
1646
+ let responseHighlightEnabled = false;
1624
1647
  let editorHighlightRenderRaf = null;
1625
1648
 
1626
1649
  function getIdleStatus() {
@@ -1946,6 +1969,19 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1946
1969
  return;
1947
1970
  }
1948
1971
 
1972
+ if (responseHighlightEnabled) {
1973
+ if (markdown.length > RESPONSE_HIGHLIGHT_MAX_CHARS) {
1974
+ critiqueViewEl.innerHTML = buildPreviewErrorHtml(
1975
+ "Response is too large for markdown highlighting. Showing plain markdown.",
1976
+ markdown,
1977
+ );
1978
+ return;
1979
+ }
1980
+
1981
+ critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightMarkdown(markdown) + "</div>";
1982
+ return;
1983
+ }
1984
+
1949
1985
  critiqueViewEl.innerHTML = buildPlainMarkdownHtml(markdown);
1950
1986
  }
1951
1987
 
@@ -2005,6 +2041,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2005
2041
  editorViewSelect.disabled = uiBusy;
2006
2042
  rightViewSelect.disabled = uiBusy;
2007
2043
  followSelect.disabled = uiBusy;
2044
+ if (responseHighlightSelect) responseHighlightSelect.disabled = uiBusy || rightView !== "markdown";
2008
2045
  insertHeaderBtn.disabled = uiBusy;
2009
2046
  critiqueBtn.disabled = uiBusy;
2010
2047
  lensSelect.disabled = uiBusy;
@@ -2052,6 +2089,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2052
2089
  rightView = nextView === "preview" ? "preview" : "markdown";
2053
2090
  rightViewSelect.value = rightView;
2054
2091
  renderActiveResult();
2092
+ syncActionButtons();
2055
2093
  }
2056
2094
 
2057
2095
  function getToken() {
@@ -2225,24 +2263,40 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2225
2263
  sourceHighlightEl.scrollLeft = sourceTextEl.scrollLeft;
2226
2264
  }
2227
2265
 
2228
- function readStoredEditorHighlightEnabled() {
2266
+ function readStoredToggle(storageKey) {
2229
2267
  if (!window.localStorage) return false;
2230
2268
  try {
2231
- return window.localStorage.getItem(EDITOR_HIGHLIGHT_STORAGE_KEY) === "on";
2269
+ return window.localStorage.getItem(storageKey) === "on";
2232
2270
  } catch {
2233
2271
  return false;
2234
2272
  }
2235
2273
  }
2236
2274
 
2237
- function persistEditorHighlightEnabled(enabled) {
2275
+ function persistStoredToggle(storageKey, enabled) {
2238
2276
  if (!window.localStorage) return;
2239
2277
  try {
2240
- window.localStorage.setItem(EDITOR_HIGHLIGHT_STORAGE_KEY, enabled ? "on" : "off");
2278
+ window.localStorage.setItem(storageKey, enabled ? "on" : "off");
2241
2279
  } catch {
2242
2280
  // ignore storage failures
2243
2281
  }
2244
2282
  }
2245
2283
 
2284
+ function readStoredEditorHighlightEnabled() {
2285
+ return readStoredToggle(EDITOR_HIGHLIGHT_STORAGE_KEY);
2286
+ }
2287
+
2288
+ function readStoredResponseHighlightEnabled() {
2289
+ return readStoredToggle(RESPONSE_HIGHLIGHT_STORAGE_KEY);
2290
+ }
2291
+
2292
+ function persistEditorHighlightEnabled(enabled) {
2293
+ persistStoredToggle(EDITOR_HIGHLIGHT_STORAGE_KEY, enabled);
2294
+ }
2295
+
2296
+ function persistResponseHighlightEnabled(enabled) {
2297
+ persistStoredToggle(RESPONSE_HIGHLIGHT_STORAGE_KEY, enabled);
2298
+ }
2299
+
2246
2300
  function updateEditorHighlightState() {
2247
2301
  const enabled = editorHighlightEnabled && editorView === "markdown";
2248
2302
 
@@ -2283,6 +2337,15 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2283
2337
  updateEditorHighlightState();
2284
2338
  }
2285
2339
 
2340
+ function setResponseHighlightEnabled(enabled) {
2341
+ responseHighlightEnabled = Boolean(enabled);
2342
+ persistResponseHighlightEnabled(responseHighlightEnabled);
2343
+ if (responseHighlightSelect) {
2344
+ responseHighlightSelect.value = responseHighlightEnabled ? "on" : "off";
2345
+ }
2346
+ renderActiveResult();
2347
+ }
2348
+
2286
2349
  function extractSection(markdown, title) {
2287
2350
  if (!markdown || !title) return "";
2288
2351
 
@@ -2726,6 +2789,12 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2726
2789
  });
2727
2790
  }
2728
2791
 
2792
+ if (responseHighlightSelect) {
2793
+ responseHighlightSelect.addEventListener("change", () => {
2794
+ setResponseHighlightEnabled(responseHighlightSelect.value === "on");
2795
+ });
2796
+ }
2797
+
2729
2798
  pullLatestBtn.addEventListener("click", () => {
2730
2799
  if (queuedLatestResponse) {
2731
2800
  if (applyLatestPayload(queuedLatestResponse)) {
@@ -2977,6 +3046,10 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2977
3046
  || Boolean(highlightSelect && highlightSelect.value === "on");
2978
3047
  setEditorHighlightEnabled(initialHighlightEnabled);
2979
3048
 
3049
+ const initialResponseHighlightEnabled = readStoredResponseHighlightEnabled()
3050
+ || Boolean(responseHighlightSelect && responseHighlightSelect.value === "on");
3051
+ setResponseHighlightEnabled(initialResponseHighlightEnabled);
3052
+
2980
3053
  setEditorView(editorView);
2981
3054
  setRightView(rightView);
2982
3055
  renderSourcePreview();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",