pi-studio 0.1.4 → 0.1.6

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,7 +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.
25
+ - Response-side markdown highlighting toggle (`Highlight markdown: Off|On`) in `Response: Markdown` view, with local preference persistence.
26
+ - Markdown highlighter now applies lightweight fenced-code token colors for common languages (`js/ts`, `python`, `bash/sh`, `json`).
26
27
  - Obsidian wiki-image syntax normalization (`![[path]]`, `![[path|alt]]`) before pandoc preview rendering.
27
28
 
28
29
  ### Changed
@@ -32,9 +33,11 @@ All notable changes to `pi-studio` are documented here.
32
33
  - Editor sync badge now tracks relation to latest response (`No response loaded`, `In sync with response`, `Edited since response`).
33
34
  - Footer continues to show explicit WS phase (`Connecting`, `Ready`, `Submitting`, `Disconnected`) alongside status text.
34
35
  - Running text and preparing annotated scaffolds are now separate explicit actions (no hidden header wrapping on send).
36
+ - Renamed file-backed header action from **Save Over** to **Save file**, with tooltip showing the current overwrite target.
35
37
  - Critique-specific load actions now focus on notes/full views and are only shown for structured critique responses.
36
38
  - Studio still live-updates latest response when assistant output arrives outside studio requests (e.g., manual send from pi editor).
37
39
  - Preview pane typography/style now follows the higher-fidelity `/preview-browser` rendering style more closely.
40
+ - Preview mode now uses pandoc code highlighting output for syntax-colored code blocks.
38
41
  - Hardened Studio preview HTTP handling and added client-side preview-request timeout to avoid stuck "Rendering preview…" states.
39
42
 
40
43
  ### Fixed
package/README.md CHANGED
@@ -31,9 +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
+ - Optional markdown highlighting toggles for editor and response markdown views (including fenced-code token colors for common languages)
37
37
  - Theme-aware browser UI based on current pi theme
38
38
 
39
39
  ## Commands
@@ -73,7 +73,7 @@ pi -e https://github.com/omaclaren/pi-studio
73
73
  - Local-only server (`127.0.0.1`) with rotating session tokens.
74
74
  - One studio request at a time.
75
75
  - Studio URLs include a token query parameter; avoid sharing full Studio URLs.
76
- - Preview panes render markdown via `pandoc` (`gfm+tex_math_dollars` → HTML5 + MathML), sanitized in-browser with `dompurify`.
76
+ - Preview panes render markdown via `pandoc` (`gfm+tex_math_dollars` → HTML5 + MathML), including pandoc code syntax highlighting, sanitized in-browser with `dompurify`.
77
77
  - Preview rendering normalizes Obsidian wiki-image syntax (`![[path]]`, `![[path|alt]]`) into standard markdown images.
78
78
  - Install pandoc for full preview rendering (`brew install pandoc` on macOS).
79
79
  - If `pandoc` is unavailable, preview falls back to plain markdown text with an inline warning.
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
@@ -499,7 +499,7 @@ function normalizeObsidianImages(markdown: string): string {
499
499
 
500
500
  async function renderStudioMarkdownWithPandoc(markdown: string): Promise<string> {
501
501
  const pandocCommand = process.env.PANDOC_PATH?.trim() || "pandoc";
502
- const args = ["-f", "gfm+tex_math_dollars-raw_html", "-t", "html5", "--mathml", "--no-highlight"];
502
+ const args = ["-f", "gfm+tex_math_dollars-raw_html", "-t", "html5", "--mathml"];
503
503
  const normalizedMarkdown = normalizeObsidianImages(normalizeMathDelimiters(markdown));
504
504
 
505
505
  return await new Promise<string>((resolve, reject) => {
@@ -1217,6 +1217,29 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1217
1217
  color: var(--ok);
1218
1218
  }
1219
1219
 
1220
+ .hl-code-kw {
1221
+ color: var(--accent);
1222
+ font-weight: 600;
1223
+ }
1224
+
1225
+ .hl-code-str {
1226
+ color: var(--ok);
1227
+ }
1228
+
1229
+ .hl-code-num {
1230
+ color: var(--warn);
1231
+ }
1232
+
1233
+ .hl-code-com {
1234
+ color: var(--muted);
1235
+ font-style: italic;
1236
+ }
1237
+
1238
+ .hl-code-var,
1239
+ .hl-code-key {
1240
+ color: var(--accent);
1241
+ }
1242
+
1220
1243
  .hl-list {
1221
1244
  color: var(--accent);
1222
1245
  font-weight: 600;
@@ -1329,6 +1352,48 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1329
1352
  padding: 0.12em 0.35em;
1330
1353
  }
1331
1354
 
1355
+ .rendered-markdown code span.kw,
1356
+ .rendered-markdown code span.cf,
1357
+ .rendered-markdown code span.im,
1358
+ .rendered-markdown code span.dt {
1359
+ color: var(--accent);
1360
+ font-weight: 600;
1361
+ }
1362
+
1363
+ .rendered-markdown code span.fu,
1364
+ .rendered-markdown code span.bu,
1365
+ .rendered-markdown code span.va,
1366
+ .rendered-markdown code span.ot {
1367
+ color: var(--accent);
1368
+ }
1369
+
1370
+ .rendered-markdown code span.st,
1371
+ .rendered-markdown code span.ss,
1372
+ .rendered-markdown code span.sc {
1373
+ color: var(--ok);
1374
+ }
1375
+
1376
+ .rendered-markdown code span.dv,
1377
+ .rendered-markdown code span.bn,
1378
+ .rendered-markdown code span.fl {
1379
+ color: var(--warn);
1380
+ }
1381
+
1382
+ .rendered-markdown code span.co {
1383
+ color: var(--muted);
1384
+ font-style: italic;
1385
+ }
1386
+
1387
+ .rendered-markdown code span.op {
1388
+ color: var(--text);
1389
+ }
1390
+
1391
+ .rendered-markdown code span.er,
1392
+ .rendered-markdown code span.al {
1393
+ color: var(--error);
1394
+ font-weight: 600;
1395
+ }
1396
+
1332
1397
  .rendered-markdown table {
1333
1398
  border-collapse: collapse;
1334
1399
  display: block;
@@ -1477,9 +1542,9 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1477
1542
  <option value="markdown">Response: Markdown</option>
1478
1543
  <option value="preview" selected>Response: Preview</option>
1479
1544
  </select>
1480
- <button id="saveAsBtn" type="button">Save As…</button>
1481
- <button id="saveOverBtn" type="button" disabled>Save Over</button>
1482
- <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>
1545
+ <button id="saveAsBtn" type="button" title="Save editor text to a new file path.">Save As…</button>
1546
+ <button id="saveOverBtn" type="button" title="Overwrite current file with editor text." disabled>Save file</button>
1547
+ <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>
1483
1548
  </div>
1484
1549
  </header>
1485
1550
 
@@ -1502,10 +1567,10 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1502
1567
  </select>
1503
1568
  <button id="critiqueBtn" type="button">Critique editor text</button>
1504
1569
  <button id="sendEditorBtn" type="button">Send to pi editor</button>
1505
- <button id="copyDraftBtn" type="button">Copy editor</button>
1570
+ <button id="copyDraftBtn" type="button">Copy editor text</button>
1506
1571
  <select id="highlightSelect" aria-label="Editor syntax highlighting">
1507
- <option value="off" selected>Highlight editor: Off</option>
1508
- <option value="on">Highlight editor: On</option>
1572
+ <option value="off" selected>Highlight markdown: Off</option>
1573
+ <option value="on">Highlight markdown: On</option>
1509
1574
  </select>
1510
1575
  </div>
1511
1576
  </div>
@@ -1530,14 +1595,14 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
1530
1595
  <option value="off">Auto-update response: Off</option>
1531
1596
  </select>
1532
1597
  <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>
1598
+ <option value="off" selected>Highlight markdown: Off</option>
1599
+ <option value="on">Highlight markdown: On</option>
1535
1600
  </select>
1536
1601
  <button id="pullLatestBtn" type="button" title="Fetch the latest assistant response when auto-update is off.">Get latest response</button>
1537
1602
  <button id="loadResponseBtn" type="button">Load response into editor</button>
1538
1603
  <button id="loadCritiqueNotesBtn" type="button" hidden>Load critique (notes)</button>
1539
1604
  <button id="loadCritiqueFullBtn" type="button" hidden>Load critique (full)</button>
1540
- <button id="copyResponseBtn" type="button">Copy response</button>
1605
+ <button id="copyResponseBtn" type="button">Copy response text</button>
1541
1606
  </div>
1542
1607
  </div>
1543
1608
  </section>
@@ -2030,10 +2095,25 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2030
2095
  updateResultActionButtons();
2031
2096
  }
2032
2097
 
2098
+ function updateSaveFileTooltip() {
2099
+ if (!saveOverBtn) return;
2100
+
2101
+ const isFileBacked = sourceState.source === "file" && Boolean(sourceState.path);
2102
+ if (isFileBacked) {
2103
+ const target = sourceState.label || sourceState.path;
2104
+ saveOverBtn.title = "Overwrite current file: " + target;
2105
+ return;
2106
+ }
2107
+
2108
+ saveOverBtn.title = "Save file is available after opening a file or using Save As…";
2109
+ }
2110
+
2033
2111
  function syncActionButtons() {
2112
+ const canSaveOver = sourceState.source === "file" && Boolean(sourceState.path);
2113
+
2034
2114
  fileInput.disabled = uiBusy;
2035
2115
  saveAsBtn.disabled = uiBusy;
2036
- saveOverBtn.disabled = uiBusy || !(sourceState.source === "file" && sourceState.path);
2116
+ saveOverBtn.disabled = uiBusy || !canSaveOver;
2037
2117
  sendEditorBtn.disabled = uiBusy;
2038
2118
  sendRunBtn.disabled = uiBusy;
2039
2119
  copyDraftBtn.disabled = uiBusy;
@@ -2045,6 +2125,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2045
2125
  insertHeaderBtn.disabled = uiBusy;
2046
2126
  critiqueBtn.disabled = uiBusy;
2047
2127
  lensSelect.disabled = uiBusy;
2128
+ updateSaveFileTooltip();
2048
2129
  updateResultActionButtons();
2049
2130
  }
2050
2131
 
@@ -2157,12 +2238,119 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2157
2238
  return out;
2158
2239
  }
2159
2240
 
2241
+ function normalizeFenceLanguage(info) {
2242
+ const raw = String(info || "").trim();
2243
+ if (!raw) return "";
2244
+
2245
+ const first = raw.split(/\\s+/)[0].replace(/^\\./, "").toLowerCase();
2246
+
2247
+ if (first === "js" || first === "javascript" || first === "jsx" || first === "node") return "javascript";
2248
+ if (first === "ts" || first === "typescript" || first === "tsx") return "typescript";
2249
+ if (first === "py" || first === "python") return "python";
2250
+ if (first === "sh" || first === "bash" || first === "zsh" || first === "shell") return "bash";
2251
+ if (first === "json" || first === "jsonc") return "json";
2252
+
2253
+ return "";
2254
+ }
2255
+
2256
+ function highlightCodeTokens(line, pattern, classifyMatch) {
2257
+ const source = String(line || "");
2258
+ let out = "";
2259
+ let lastIndex = 0;
2260
+ pattern.lastIndex = 0;
2261
+
2262
+ let match;
2263
+ while ((match = pattern.exec(source)) !== null) {
2264
+ const token = match[0] || "";
2265
+ const start = typeof match.index === "number" ? match.index : 0;
2266
+
2267
+ if (start > lastIndex) {
2268
+ out += escapeHtml(source.slice(lastIndex, start));
2269
+ }
2270
+
2271
+ const className = classifyMatch(match) || "hl-code";
2272
+ out += wrapHighlight(className, token);
2273
+
2274
+ lastIndex = start + token.length;
2275
+ if (token.length === 0) {
2276
+ pattern.lastIndex += 1;
2277
+ }
2278
+ }
2279
+
2280
+ if (lastIndex < source.length) {
2281
+ out += escapeHtml(source.slice(lastIndex));
2282
+ }
2283
+
2284
+ return out;
2285
+ }
2286
+
2287
+ function highlightCodeLine(line, language) {
2288
+ const source = String(line || "");
2289
+ const lang = normalizeFenceLanguage(language);
2290
+
2291
+ if (!lang) {
2292
+ return wrapHighlight("hl-code", source);
2293
+ }
2294
+
2295
+ if (lang === "javascript" || lang === "typescript") {
2296
+ const jsPattern = /(\\/\\/.*$)|("(?:[^"\\\\]|\\\\.)*"|'(?:[^'\\\\]|\\\\.)*')|(\\b(?:const|let|var|function|return|if|else|for|while|switch|case|break|continue|try|catch|finally|throw|new|class|extends|import|from|export|default|async|await|true|false|null|undefined|typeof|instanceof)\\b)|(\\b\\d+(?:\\.\\d+)?\\b)/g;
2297
+ const highlighted = highlightCodeTokens(source, jsPattern, (match) => {
2298
+ if (match[1]) return "hl-code-com";
2299
+ if (match[2]) return "hl-code-str";
2300
+ if (match[3]) return "hl-code-kw";
2301
+ if (match[4]) return "hl-code-num";
2302
+ return "hl-code";
2303
+ });
2304
+ return "<span class='hl-code'>" + highlighted + "</span>";
2305
+ }
2306
+
2307
+ if (lang === "python") {
2308
+ const pyPattern = /(#.*$)|("(?:[^"\\\\]|\\\\.)*"|'(?:[^'\\\\]|\\\\.)*')|(\\b(?:def|class|return|if|elif|else|for|while|try|except|finally|import|from|as|with|lambda|yield|True|False|None|and|or|not|in|is|pass|break|continue|raise|global|nonlocal|assert)\\b)|(\\b\\d+(?:\\.\\d+)?\\b)/g;
2309
+ const highlighted = highlightCodeTokens(source, pyPattern, (match) => {
2310
+ if (match[1]) return "hl-code-com";
2311
+ if (match[2]) return "hl-code-str";
2312
+ if (match[3]) return "hl-code-kw";
2313
+ if (match[4]) return "hl-code-num";
2314
+ return "hl-code";
2315
+ });
2316
+ return "<span class='hl-code'>" + highlighted + "</span>";
2317
+ }
2318
+
2319
+ if (lang === "bash") {
2320
+ const shPattern = /(#.*$)|("(?:[^"\\\\]|\\\\.)*"|'[^']*')|(\\$\\{[^}]+\\}|\\$[A-Za-z_][A-Za-z0-9_]*)|(\\b(?:if|then|else|fi|for|in|do|done|case|esac|function|local|export|readonly|return|break|continue|while|until)\\b)|(\\b\\d+\\b)/g;
2321
+ const highlighted = highlightCodeTokens(source, shPattern, (match) => {
2322
+ if (match[1]) return "hl-code-com";
2323
+ if (match[2]) return "hl-code-str";
2324
+ if (match[3]) return "hl-code-var";
2325
+ if (match[4]) return "hl-code-kw";
2326
+ if (match[5]) return "hl-code-num";
2327
+ return "hl-code";
2328
+ });
2329
+ return "<span class='hl-code'>" + highlighted + "</span>";
2330
+ }
2331
+
2332
+ if (lang === "json") {
2333
+ const jsonPattern = /("(?:[^"\\\\]|\\\\.)*"\\s*:)|("(?:[^"\\\\]|\\\\.)*")|(\\b(?:true|false|null)\\b)|(\\b-?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?\\b)/g;
2334
+ const highlighted = highlightCodeTokens(source, jsonPattern, (match) => {
2335
+ if (match[1]) return "hl-code-key";
2336
+ if (match[2]) return "hl-code-str";
2337
+ if (match[3]) return "hl-code-kw";
2338
+ if (match[4]) return "hl-code-num";
2339
+ return "hl-code";
2340
+ });
2341
+ return "<span class='hl-code'>" + highlighted + "</span>";
2342
+ }
2343
+
2344
+ return wrapHighlight("hl-code", source);
2345
+ }
2346
+
2160
2347
  function highlightMarkdown(text) {
2161
2348
  const lines = String(text || "").replace(/\\r\\n/g, "\\n").split("\\n");
2162
2349
  const out = [];
2163
2350
  let inFence = false;
2164
2351
  let fenceChar = null;
2165
2352
  let fenceLength = 0;
2353
+ let fenceLanguage = "";
2166
2354
 
2167
2355
  for (const line of lines) {
2168
2356
  const fenceMatch = line.match(/^(\\s*)([\\x60]{3,}|~{3,})(.*)$/);
@@ -2175,10 +2363,12 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2175
2363
  inFence = true;
2176
2364
  fenceChar = markerChar;
2177
2365
  fenceLength = markerLength;
2366
+ fenceLanguage = normalizeFenceLanguage(fenceMatch[3] || "");
2178
2367
  } else if (fenceChar === markerChar && markerLength >= fenceLength) {
2179
2368
  inFence = false;
2180
2369
  fenceChar = null;
2181
2370
  fenceLength = 0;
2371
+ fenceLanguage = "";
2182
2372
  }
2183
2373
 
2184
2374
  out.push(wrapHighlight("hl-fence", line));
@@ -2186,7 +2376,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2186
2376
  }
2187
2377
 
2188
2378
  if (inFence) {
2189
- out.push(line.length > 0 ? wrapHighlight("hl-code", line) : "");
2379
+ out.push(line.length > 0 ? highlightCodeLine(line, fenceLanguage) : "");
2190
2380
  continue;
2191
2381
  }
2192
2382
 
@@ -2894,7 +3084,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2894
3084
 
2895
3085
  try {
2896
3086
  await navigator.clipboard.writeText(latestResponseMarkdown);
2897
- setStatus("Copied response.", "success");
3087
+ setStatus("Copied response text.", "success");
2898
3088
  } catch (error) {
2899
3089
  setStatus("Clipboard write failed.", "warning");
2900
3090
  }
@@ -2930,7 +3120,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2930
3120
 
2931
3121
  saveOverBtn.addEventListener("click", () => {
2932
3122
  if (!(sourceState.source === "file" && sourceState.path)) {
2933
- setStatus("Save Over is only available when source is a file path.", "warning");
3123
+ setStatus("Save file is only available when source is a file path.", "warning");
2934
3124
  return;
2935
3125
  }
2936
3126
 
@@ -3333,7 +3523,7 @@ export default function (pi: ExtensionAPI) {
3333
3523
  sendToClient(client, {
3334
3524
  type: "error",
3335
3525
  requestId: msg.requestId,
3336
- message: "Save Over is only available for file-backed documents.",
3526
+ message: "Save file is only available for file-backed documents.",
3337
3527
  });
3338
3528
  return;
3339
3529
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",