pi-studio 0.1.3 → 0.1.5

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
@@ -12,7 +12,7 @@ All notable changes to `pi-studio` are documented here.
12
12
  - **Load response into editor** (for non-critique responses)
13
13
  - **Load critique (notes)**
14
14
  - **Load critique (full)**
15
- - **Copy response**
15
+ - **Copy response text**
16
16
  - Independent Markdown/Preview toggles for Editor and right pane.
17
17
  - `Auto-update response: On|Off` + `Get latest response` controls for terminal/editor-composability.
18
18
  - Source action: **Run editor text** to submit current editor text directly to the model.
@@ -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 markdown: 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.
@@ -30,6 +32,7 @@ All notable changes to `pi-studio` are documented here.
30
32
  - Editor sync badge now tracks relation to latest response (`No response loaded`, `In sync with response`, `Edited since response`).
31
33
  - Footer continues to show explicit WS phase (`Connecting`, `Ready`, `Submitting`, `Disconnected`) alongside status text.
32
34
  - Running text and preparing annotated scaffolds are now separate explicit actions (no hidden header wrapping on send).
35
+ - Renamed file-backed header action from **Save Over** to **Save file**, with tooltip showing the current overwrite target.
33
36
  - Critique-specific load actions now focus on notes/full views and are only shown for structured critique responses.
34
37
  - Studio still live-updates latest response when assistant output arrives outside studio requests (e.g., manual send from pi editor).
35
38
  - Preview pane typography/style now follows the higher-fidelity `/preview-browser` rendering style more closely.
package/README.md CHANGED
@@ -31,8 +31,9 @@ Status: experimental alpha.
31
31
  - Response load helpers:
32
32
  - non-critique: **Load response into editor**
33
33
  - critique: **Load critique (notes)** / **Load critique (full)**
34
- - File actions: **Save As…**, **Save Over**, **Load file in editor**
34
+ - File actions: **Save As…**, **Save file**, **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/WORKFLOW.md CHANGED
@@ -74,10 +74,10 @@ Rules:
74
74
 
75
75
  ## Required UI elements
76
76
 
77
- - Header actions: **Save As…**, **Save Over** (file-backed), **Load file in editor**
77
+ - Header actions: **Save As…**, **Save file** (file-backed), **Load file in editor**
78
78
  - Header view toggles: `Editor: Markdown|Preview`, `Response: Markdown|Preview`
79
79
  - Preview mode uses server-side `pandoc` rendering (math-aware) with plain-markdown fallback when renderer is unavailable.
80
- - Editor actions: **Insert annotation header**, **Run editor text**, **Critique editor text** (+ critique focus), **Send to pi editor**, **Copy editor**
80
+ - Editor actions: **Insert annotation header**, **Run editor text**, **Critique editor text** (+ critique focus), **Send to pi editor**, **Copy editor text**
81
81
  - Response actions include `Auto-update response: On|Off` + **Get latest response**
82
82
  - Source badge: `blank | last model response | file <path> | upload`
83
83
  - Response badge: `none | assistant response | assistant critique` (+ timestamp)
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;
@@ -1462,9 +1477,9 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1462
1477
  <option value="markdown">Response: Markdown</option>
1463
1478
  <option value="preview" selected>Response: Preview</option>
1464
1479
  </select>
1465
- <button id="saveAsBtn" type="button">Save As…</button>
1466
- <button id="saveOverBtn" type="button" disabled>Save Over</button>
1467
- <label class="file-label">Load file in editor<input id="fileInput" type="file" accept=".txt,.md,.markdown,.rst,.adoc,.tex,.json,.js,.ts,.py,.java,.c,.cpp,.go,.rs,.rb,.swift,.sh,.html,.css,.xml,.yaml,.yml,.toml" /></label>
1480
+ <button id="saveAsBtn" type="button" title="Save editor text to a new file path.">Save As…</button>
1481
+ <button id="saveOverBtn" type="button" title="Overwrite current file with editor text." disabled>Save file</button>
1482
+ <label class="file-label" title="Load a local file into editor text.">Load file in editor<input id="fileInput" type="file" accept=".txt,.md,.markdown,.rst,.adoc,.tex,.json,.js,.ts,.py,.java,.c,.cpp,.go,.rs,.rb,.swift,.sh,.html,.css,.xml,.yaml,.yml,.toml" /></label>
1468
1483
  </div>
1469
1484
  </header>
1470
1485
 
@@ -1487,10 +1502,10 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1487
1502
  </select>
1488
1503
  <button id="critiqueBtn" type="button">Critique editor text</button>
1489
1504
  <button id="sendEditorBtn" type="button">Send to pi editor</button>
1490
- <button id="copyDraftBtn" type="button">Copy editor</button>
1505
+ <button id="copyDraftBtn" type="button">Copy editor text</button>
1491
1506
  <select id="highlightSelect" aria-label="Editor syntax highlighting">
1492
- <option value="off" selected>Highlight editor: Off</option>
1493
- <option value="on">Highlight editor: On</option>
1507
+ <option value="off" selected>Highlight markdown: Off</option>
1508
+ <option value="on">Highlight markdown: On</option>
1494
1509
  </select>
1495
1510
  </div>
1496
1511
  </div>
@@ -1514,11 +1529,15 @@ 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 markdown: Off</option>
1534
+ <option value="on">Highlight markdown: 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>
1520
1539
  <button id="loadCritiqueFullBtn" type="button" hidden>Load critique (full)</button>
1521
- <button id="copyResponseBtn" type="button">Copy response</button>
1540
+ <button id="copyResponseBtn" type="button">Copy response text</button>
1522
1541
  </div>
1523
1542
  </div>
1524
1543
  </section>
@@ -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
 
@@ -1994,10 +2030,25 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1994
2030
  updateResultActionButtons();
1995
2031
  }
1996
2032
 
2033
+ function updateSaveFileTooltip() {
2034
+ if (!saveOverBtn) return;
2035
+
2036
+ const isFileBacked = sourceState.source === "file" && Boolean(sourceState.path);
2037
+ if (isFileBacked) {
2038
+ const target = sourceState.label || sourceState.path;
2039
+ saveOverBtn.title = "Overwrite current file: " + target;
2040
+ return;
2041
+ }
2042
+
2043
+ saveOverBtn.title = "Save file is available after opening a file or using Save As…";
2044
+ }
2045
+
1997
2046
  function syncActionButtons() {
2047
+ const canSaveOver = sourceState.source === "file" && Boolean(sourceState.path);
2048
+
1998
2049
  fileInput.disabled = uiBusy;
1999
2050
  saveAsBtn.disabled = uiBusy;
2000
- saveOverBtn.disabled = uiBusy || !(sourceState.source === "file" && sourceState.path);
2051
+ saveOverBtn.disabled = uiBusy || !canSaveOver;
2001
2052
  sendEditorBtn.disabled = uiBusy;
2002
2053
  sendRunBtn.disabled = uiBusy;
2003
2054
  copyDraftBtn.disabled = uiBusy;
@@ -2005,9 +2056,11 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2005
2056
  editorViewSelect.disabled = uiBusy;
2006
2057
  rightViewSelect.disabled = uiBusy;
2007
2058
  followSelect.disabled = uiBusy;
2059
+ if (responseHighlightSelect) responseHighlightSelect.disabled = uiBusy || rightView !== "markdown";
2008
2060
  insertHeaderBtn.disabled = uiBusy;
2009
2061
  critiqueBtn.disabled = uiBusy;
2010
2062
  lensSelect.disabled = uiBusy;
2063
+ updateSaveFileTooltip();
2011
2064
  updateResultActionButtons();
2012
2065
  }
2013
2066
 
@@ -2052,6 +2105,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2052
2105
  rightView = nextView === "preview" ? "preview" : "markdown";
2053
2106
  rightViewSelect.value = rightView;
2054
2107
  renderActiveResult();
2108
+ syncActionButtons();
2055
2109
  }
2056
2110
 
2057
2111
  function getToken() {
@@ -2225,24 +2279,40 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2225
2279
  sourceHighlightEl.scrollLeft = sourceTextEl.scrollLeft;
2226
2280
  }
2227
2281
 
2228
- function readStoredEditorHighlightEnabled() {
2282
+ function readStoredToggle(storageKey) {
2229
2283
  if (!window.localStorage) return false;
2230
2284
  try {
2231
- return window.localStorage.getItem(EDITOR_HIGHLIGHT_STORAGE_KEY) === "on";
2285
+ return window.localStorage.getItem(storageKey) === "on";
2232
2286
  } catch {
2233
2287
  return false;
2234
2288
  }
2235
2289
  }
2236
2290
 
2237
- function persistEditorHighlightEnabled(enabled) {
2291
+ function persistStoredToggle(storageKey, enabled) {
2238
2292
  if (!window.localStorage) return;
2239
2293
  try {
2240
- window.localStorage.setItem(EDITOR_HIGHLIGHT_STORAGE_KEY, enabled ? "on" : "off");
2294
+ window.localStorage.setItem(storageKey, enabled ? "on" : "off");
2241
2295
  } catch {
2242
2296
  // ignore storage failures
2243
2297
  }
2244
2298
  }
2245
2299
 
2300
+ function readStoredEditorHighlightEnabled() {
2301
+ return readStoredToggle(EDITOR_HIGHLIGHT_STORAGE_KEY);
2302
+ }
2303
+
2304
+ function readStoredResponseHighlightEnabled() {
2305
+ return readStoredToggle(RESPONSE_HIGHLIGHT_STORAGE_KEY);
2306
+ }
2307
+
2308
+ function persistEditorHighlightEnabled(enabled) {
2309
+ persistStoredToggle(EDITOR_HIGHLIGHT_STORAGE_KEY, enabled);
2310
+ }
2311
+
2312
+ function persistResponseHighlightEnabled(enabled) {
2313
+ persistStoredToggle(RESPONSE_HIGHLIGHT_STORAGE_KEY, enabled);
2314
+ }
2315
+
2246
2316
  function updateEditorHighlightState() {
2247
2317
  const enabled = editorHighlightEnabled && editorView === "markdown";
2248
2318
 
@@ -2283,6 +2353,15 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2283
2353
  updateEditorHighlightState();
2284
2354
  }
2285
2355
 
2356
+ function setResponseHighlightEnabled(enabled) {
2357
+ responseHighlightEnabled = Boolean(enabled);
2358
+ persistResponseHighlightEnabled(responseHighlightEnabled);
2359
+ if (responseHighlightSelect) {
2360
+ responseHighlightSelect.value = responseHighlightEnabled ? "on" : "off";
2361
+ }
2362
+ renderActiveResult();
2363
+ }
2364
+
2286
2365
  function extractSection(markdown, title) {
2287
2366
  if (!markdown || !title) return "";
2288
2367
 
@@ -2726,6 +2805,12 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2726
2805
  });
2727
2806
  }
2728
2807
 
2808
+ if (responseHighlightSelect) {
2809
+ responseHighlightSelect.addEventListener("change", () => {
2810
+ setResponseHighlightEnabled(responseHighlightSelect.value === "on");
2811
+ });
2812
+ }
2813
+
2729
2814
  pullLatestBtn.addEventListener("click", () => {
2730
2815
  if (queuedLatestResponse) {
2731
2816
  if (applyLatestPayload(queuedLatestResponse)) {
@@ -2825,7 +2910,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2825
2910
 
2826
2911
  try {
2827
2912
  await navigator.clipboard.writeText(latestResponseMarkdown);
2828
- setStatus("Copied response.", "success");
2913
+ setStatus("Copied response text.", "success");
2829
2914
  } catch (error) {
2830
2915
  setStatus("Clipboard write failed.", "warning");
2831
2916
  }
@@ -2861,7 +2946,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2861
2946
 
2862
2947
  saveOverBtn.addEventListener("click", () => {
2863
2948
  if (!(sourceState.source === "file" && sourceState.path)) {
2864
- setStatus("Save Over is only available when source is a file path.", "warning");
2949
+ setStatus("Save file is only available when source is a file path.", "warning");
2865
2950
  return;
2866
2951
  }
2867
2952
 
@@ -2977,6 +3062,10 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2977
3062
  || Boolean(highlightSelect && highlightSelect.value === "on");
2978
3063
  setEditorHighlightEnabled(initialHighlightEnabled);
2979
3064
 
3065
+ const initialResponseHighlightEnabled = readStoredResponseHighlightEnabled()
3066
+ || Boolean(responseHighlightSelect && responseHighlightSelect.value === "on");
3067
+ setResponseHighlightEnabled(initialResponseHighlightEnabled);
3068
+
2980
3069
  setEditorView(editorView);
2981
3070
  setRightView(rightView);
2982
3071
  renderSourcePreview();
@@ -3260,7 +3349,7 @@ export default function (pi: ExtensionAPI) {
3260
3349
  sendToClient(client, {
3261
3350
  type: "error",
3262
3351
  requestId: msg.requestId,
3263
- message: "Save Over is only available for file-backed documents.",
3352
+ message: "Save file is only available for file-backed documents.",
3264
3353
  });
3265
3354
  return;
3266
3355
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",