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 +5 -2
- package/README.md +3 -3
- package/WORKFLOW.md +2 -2
- package/index.ts +205 -15
- package/package.json +1 -1
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
|
|
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
|
|
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
|
|
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"
|
|
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
|
|
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
|
|
1508
|
-
<option value="on">Highlight
|
|
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
|
|
1534
|
-
<option value="on">Highlight
|
|
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 || !
|
|
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 ?
|
|
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
|
|
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
|
|
3526
|
+
message: "Save file is only available for file-backed documents.",
|
|
3337
3527
|
});
|
|
3338
3528
|
return;
|
|
3339
3529
|
}
|