pi-studio 0.1.5 → 0.1.7

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
@@ -23,6 +23,7 @@ All notable changes to `pi-studio` are documented here.
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
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
@@ -36,6 +37,7 @@ All notable changes to `pi-studio` are documented here.
36
37
  - Critique-specific load actions now focus on notes/full views and are only shown for structured critique responses.
37
38
  - Studio still live-updates latest response when assistant output arrives outside studio requests (e.g., manual send from pi editor).
38
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.
39
41
  - Hardened Studio preview HTTP handling and added client-side preview-request timeout to avoid stuck "Rendering preview…" states.
40
42
 
41
43
  ### Fixed
package/README.md CHANGED
@@ -33,7 +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
- - 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
@@ -72,8 +72,9 @@ pi -e https://github.com/omaclaren/pi-studio
72
72
 
73
73
  - Local-only server (`127.0.0.1`) with rotating session tokens.
74
74
  - One studio request at a time.
75
+ - 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.
75
76
  - 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`.
77
+ - Preview panes render markdown via `pandoc` (`gfm+tex_math_dollars` → HTML5 + MathML), including pandoc code syntax highlighting, sanitized in-browser with `dompurify`.
77
78
  - Preview rendering normalizes Obsidian wiki-image syntax (`![[path]]`, `![[path|alt]]`) into standard markdown images.
78
79
  - Install pandoc for full preview rendering (`brew install pandoc` on macOS).
79
80
  - If `pandoc` is unavailable, preview falls back to plain markdown text with an inline warning.
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;
@@ -2173,12 +2238,119 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2173
2238
  return out;
2174
2239
  }
2175
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
+
2176
2347
  function highlightMarkdown(text) {
2177
2348
  const lines = String(text || "").replace(/\\r\\n/g, "\\n").split("\\n");
2178
2349
  const out = [];
2179
2350
  let inFence = false;
2180
2351
  let fenceChar = null;
2181
2352
  let fenceLength = 0;
2353
+ let fenceLanguage = "";
2182
2354
 
2183
2355
  for (const line of lines) {
2184
2356
  const fenceMatch = line.match(/^(\\s*)([\\x60]{3,}|~{3,})(.*)$/);
@@ -2191,10 +2363,12 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2191
2363
  inFence = true;
2192
2364
  fenceChar = markerChar;
2193
2365
  fenceLength = markerLength;
2366
+ fenceLanguage = normalizeFenceLanguage(fenceMatch[3] || "");
2194
2367
  } else if (fenceChar === markerChar && markerLength >= fenceLength) {
2195
2368
  inFence = false;
2196
2369
  fenceChar = null;
2197
2370
  fenceLength = 0;
2371
+ fenceLanguage = "";
2198
2372
  }
2199
2373
 
2200
2374
  out.push(wrapHighlight("hl-fence", line));
@@ -2202,7 +2376,7 @@ function buildStudioHtml(initialDocument: InitialStudioDocument | null, theme?:
2202
2376
  }
2203
2377
 
2204
2378
  if (inFence) {
2205
- out.push(line.length > 0 ? wrapHighlight("hl-code", line) : "");
2379
+ out.push(line.length > 0 ? highlightCodeLine(line, fenceLanguage) : "");
2206
2380
  continue;
2207
2381
  }
2208
2382
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",