pi-studio 0.5.46 → 0.5.48
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 +19 -0
- package/README.md +1 -1
- package/client/studio-client.js +176 -26
- package/client/studio.css +18 -0
- package/index.ts +40 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,25 @@ All notable changes to `pi-studio` are documented here.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.5.48] — 2026-04-07
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Editor-preview comments now also work for code, plain-text, and diff files using line-based preview selection anchors, so code/text review can use the same local comments workflow as Markdown preview.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- LaTeX editor preview now correctly treats `.tex`/LaTeX editor content as LaTeX in preview even when the document is only a fragment rather than a full `\documentclass` wrapper.
|
|
14
|
+
- Ordinary response preview no longer flips into LaTeX mode just because quoted LaTeX commands like `` `\\documentclass` `` or `` `\\begin{document}` `` appear in a normal response.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- LaTeX preview comments remain disabled for now; the earlier heuristic preview-side mapping attempts were removed in favor of keeping the rendering fixes and waiting for a more pipeline-grounded LaTeX comment design.
|
|
18
|
+
|
|
19
|
+
## [0.5.47] — 2026-04-07
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Raw-editor and editor-preview commenting now feel more unified: both surfaces use a contextual **Comment** action for selected text, while the dock footer keeps a de-emphasised **Line comment** fallback for current-line comments in **Editor (Raw)**.
|
|
23
|
+
- Preview-side **Comment** affordances now focus the new comment textarea more reliably after creation, including when the comments rail has to open.
|
|
24
|
+
- Clicking comment **Jump** now suppresses the raw-editor selection **Comment** pill for that programmatic selection, so jump-to-highlight does not look like a fresh comment prompt.
|
|
25
|
+
|
|
7
26
|
## [0.5.46] — 2026-04-07
|
|
8
27
|
|
|
9
28
|
### Added
|
package/README.md
CHANGED
|
@@ -18,7 +18,7 @@ Extension for [pi](https://pi.dev) that opens a local two-pane browser workspace
|
|
|
18
18
|
- Supports one canonical full Studio view per Pi session, plus additional editor-only companion views when you just want extra editing/preview surfaces
|
|
19
19
|
- Runs editor text directly, or asks for structured critique (auto/writing/code focus)
|
|
20
20
|
- Includes a local persistent scratchpad for quick notes you want to keep out of the main editor until you're ready to copy or insert them
|
|
21
|
-
- Includes local comments anchored to selections/lines, shown in a docked **Comments** rail, with **Comment** actions from raw-editor
|
|
21
|
+
- Includes local comments anchored to selections/lines, shown in a docked **Comments** rail, with **Comment** actions from raw-editor selections plus editor-preview selections for Markdown and code/text/diff previews, alongside optional inline `[an: ...]` toggles when you want comments reflected in the document text
|
|
22
22
|
- Browses response history (`Prev/Next/Last`) and loads either:
|
|
23
23
|
- response text
|
|
24
24
|
- critique notes/full critique
|
package/client/studio-client.js
CHANGED
|
@@ -306,6 +306,9 @@
|
|
|
306
306
|
let pendingReviewNoteFocusId = null;
|
|
307
307
|
let pendingReviewNoteInlineFocusId = null;
|
|
308
308
|
let activePreviewCommentSelection = null;
|
|
309
|
+
let suppressEditorSelectionComment = false;
|
|
310
|
+
let suppressedEditorSelectionStart = null;
|
|
311
|
+
let suppressedEditorSelectionEnd = null;
|
|
309
312
|
const previewJumpHighlightState = new WeakMap();
|
|
310
313
|
const PREVIEW_ANNOTATION_PLACEHOLDER_PREFIX = "PISTUDIOANNOT";
|
|
311
314
|
const annotationHelpers = globalThis.PiStudioAnnotationHelpers;
|
|
@@ -1531,19 +1534,25 @@
|
|
|
1531
1534
|
|
|
1532
1535
|
function sanitizeRenderedHtml(html, markdown) {
|
|
1533
1536
|
const rawHtml = typeof html === "string" ? html : "";
|
|
1534
|
-
const
|
|
1535
|
-
.
|
|
1536
|
-
|
|
1537
|
+
const mathAnnotationPreserved = rawHtml.replace(/<math\b([^>]*)>([\s\S]*?)<\/math>/gi, (match, attrs, inner) => {
|
|
1538
|
+
const texAnnotationMatch = String(inner || "").match(/<annotation\b[^>]*encoding="application\/x-tex"[^>]*>([\s\S]*?)<\/annotation>/i);
|
|
1539
|
+
const texSource = texAnnotationMatch ? String(texAnnotationMatch[1] || "").trim() : "";
|
|
1540
|
+
const cleanedInner = String(inner || "")
|
|
1541
|
+
.replace(/<annotation-xml\b[\s\S]*?<\/annotation-xml>/gi, "")
|
|
1542
|
+
.replace(/<annotation\b[\s\S]*?<\/annotation>/gi, "");
|
|
1543
|
+
const texAttr = texSource ? (" data-tex-source=\"" + escapeHtml(texSource) + "\"") : "";
|
|
1544
|
+
return "<math" + attrs + texAttr + ">" + cleanedInner + "</math>";
|
|
1545
|
+
});
|
|
1537
1546
|
|
|
1538
1547
|
if (window.DOMPurify && typeof window.DOMPurify.sanitize === "function") {
|
|
1539
|
-
return window.DOMPurify.sanitize(
|
|
1548
|
+
return window.DOMPurify.sanitize(mathAnnotationPreserved, {
|
|
1540
1549
|
USE_PROFILES: {
|
|
1541
1550
|
html: true,
|
|
1542
1551
|
mathMl: true,
|
|
1543
1552
|
svg: true,
|
|
1544
1553
|
},
|
|
1545
1554
|
ADD_TAGS: ["embed"],
|
|
1546
|
-
ADD_ATTR: ["src", "type", "title", "width", "height", "style", "data-fig-align"],
|
|
1555
|
+
ADD_ATTR: ["src", "type", "title", "width", "height", "style", "data-fig-align", "data-tex-source"],
|
|
1547
1556
|
ADD_DATA_URI_TAGS: ["embed"],
|
|
1548
1557
|
});
|
|
1549
1558
|
}
|
|
@@ -2272,7 +2281,7 @@
|
|
|
2272
2281
|
}
|
|
2273
2282
|
}
|
|
2274
2283
|
|
|
2275
|
-
async function renderMarkdownWithPandoc(markdown) {
|
|
2284
|
+
async function renderMarkdownWithPandoc(markdown, options) {
|
|
2276
2285
|
const token = getToken();
|
|
2277
2286
|
if (!token) {
|
|
2278
2287
|
throw new Error("Missing Studio token in URL.");
|
|
@@ -2285,20 +2294,26 @@
|
|
|
2285
2294
|
const controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
2286
2295
|
const timeoutId = controller ? window.setTimeout(() => controller.abort(), 8000) : null;
|
|
2287
2296
|
|
|
2297
|
+
const previewOptions = options && typeof options === "object" ? options : {};
|
|
2298
|
+
|
|
2288
2299
|
let response;
|
|
2289
2300
|
try {
|
|
2290
2301
|
const effectivePath = getEffectiveSavePath();
|
|
2291
2302
|
const sourcePath = effectivePath || sourceState.path || "";
|
|
2303
|
+
const payload = {
|
|
2304
|
+
markdown: String(markdown || ""),
|
|
2305
|
+
sourcePath: sourcePath,
|
|
2306
|
+
resourceDir: (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "",
|
|
2307
|
+
};
|
|
2308
|
+
if (previewOptions.includeEditorLanguage) {
|
|
2309
|
+
payload.editorLanguage = String(editorLanguage || "");
|
|
2310
|
+
}
|
|
2292
2311
|
response = await fetch("/render-preview?token=" + encodeURIComponent(token), {
|
|
2293
2312
|
method: "POST",
|
|
2294
2313
|
headers: {
|
|
2295
2314
|
"Content-Type": "application/json",
|
|
2296
2315
|
},
|
|
2297
|
-
body: JSON.stringify(
|
|
2298
|
-
markdown: String(markdown || ""),
|
|
2299
|
-
sourcePath: sourcePath,
|
|
2300
|
-
resourceDir: (!sourcePath && resourceDirInput) ? resourceDirInput.value.trim() : "",
|
|
2301
|
-
}),
|
|
2316
|
+
body: JSON.stringify(payload),
|
|
2302
2317
|
signal: controller ? controller.signal : undefined,
|
|
2303
2318
|
});
|
|
2304
2319
|
} catch (error) {
|
|
@@ -2523,7 +2538,9 @@
|
|
|
2523
2538
|
: { markdown: stripAnnotationMarkers(String(markdown || "")), placeholders: [] };
|
|
2524
2539
|
|
|
2525
2540
|
try {
|
|
2526
|
-
const renderedHtml = await renderMarkdownWithPandoc(previewPrepared.markdown
|
|
2541
|
+
const renderedHtml = await renderMarkdownWithPandoc(previewPrepared.markdown, {
|
|
2542
|
+
includeEditorLanguage: pane === "source" || rightView === "editor-preview",
|
|
2543
|
+
});
|
|
2527
2544
|
|
|
2528
2545
|
if (pane === "source") {
|
|
2529
2546
|
if (nonce !== sourcePreviewRenderNonce || editorView !== "preview") return;
|
|
@@ -2588,9 +2605,8 @@
|
|
|
2588
2605
|
function renderSourcePreviewNow() {
|
|
2589
2606
|
if (editorView !== "preview") return;
|
|
2590
2607
|
const text = prepareEditorTextForPreview(sourceTextEl.value || "");
|
|
2591
|
-
if (
|
|
2592
|
-
|
|
2593
|
-
sourcePreviewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightCode(text, editorLanguage, "preview") + "</div>";
|
|
2608
|
+
if (supportsCodePreviewCommentsForCurrentEditor()) {
|
|
2609
|
+
renderCodePreviewWithCommentBlocks(sourcePreviewEl, text, "source");
|
|
2594
2610
|
return;
|
|
2595
2611
|
}
|
|
2596
2612
|
const nonce = ++sourcePreviewRenderNonce;
|
|
@@ -2657,10 +2673,8 @@
|
|
|
2657
2673
|
scheduleResponsePaneRepaintNudge();
|
|
2658
2674
|
return;
|
|
2659
2675
|
}
|
|
2660
|
-
if (
|
|
2661
|
-
|
|
2662
|
-
critiqueViewEl.innerHTML = "<div class='response-markdown-highlight'>" + highlightCode(editorText, editorLanguage, "preview") + "</div>";
|
|
2663
|
-
scheduleResponsePaneRepaintNudge();
|
|
2676
|
+
if (supportsCodePreviewCommentsForCurrentEditor()) {
|
|
2677
|
+
renderCodePreviewWithCommentBlocks(critiqueViewEl, editorText, "response");
|
|
2664
2678
|
return;
|
|
2665
2679
|
}
|
|
2666
2680
|
const nonce = ++responsePreviewRenderNonce;
|
|
@@ -3795,6 +3809,68 @@
|
|
|
3795
3809
|
return out.join("<br>");
|
|
3796
3810
|
}
|
|
3797
3811
|
|
|
3812
|
+
function supportsCodePreviewCommentsForCurrentEditor() {
|
|
3813
|
+
return Boolean(editorLanguage) && editorLanguage !== "markdown" && editorLanguage !== "latex";
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
function getCodePreviewCommentKind(language) {
|
|
3817
|
+
const lang = normalizeFenceLanguage(language || "");
|
|
3818
|
+
if (lang === "diff") return "diff-line";
|
|
3819
|
+
if (lang === "text") return "text-line";
|
|
3820
|
+
return "code-line";
|
|
3821
|
+
}
|
|
3822
|
+
|
|
3823
|
+
function buildCodePreviewHtmlWithCommentBlocks(text, language) {
|
|
3824
|
+
const source = String(text || "").replace(/\r\n/g, "\n");
|
|
3825
|
+
const lines = source.split("\n");
|
|
3826
|
+
const lang = normalizeFenceLanguage(language || "");
|
|
3827
|
+
const kind = getCodePreviewCommentKind(lang);
|
|
3828
|
+
const html = [];
|
|
3829
|
+
let offset = 0;
|
|
3830
|
+
|
|
3831
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
3832
|
+
const line = String(lines[lineIndex] || "");
|
|
3833
|
+
const start = offset;
|
|
3834
|
+
const end = start + line.length;
|
|
3835
|
+
const lineNumber = lineIndex + 1;
|
|
3836
|
+
const lineHtml = line.length === 0
|
|
3837
|
+
? "<span class='hl-code'>" + EMPTY_OVERLAY_LINE + "</span>"
|
|
3838
|
+
: (lang ? highlightCodeLine(line, lang, "preview") : escapeHtml(line));
|
|
3839
|
+
|
|
3840
|
+
html.push(
|
|
3841
|
+
"<div class='preview-comment-block preview-comment-line-block'"
|
|
3842
|
+
+ " data-review-note-start='" + String(start) + "'"
|
|
3843
|
+
+ " data-review-note-end='" + String(end) + "'"
|
|
3844
|
+
+ " data-review-note-line-start='" + String(lineNumber) + "'"
|
|
3845
|
+
+ " data-review-note-line-end='" + String(lineNumber) + "'"
|
|
3846
|
+
+ " data-preview-comment-kind='" + escapeHtml(kind) + "'"
|
|
3847
|
+
+ ">"
|
|
3848
|
+
+ "<div class='preview-comment-controls'>"
|
|
3849
|
+
+ "<button type='button' class='preview-comment-summary' hidden></button>"
|
|
3850
|
+
+ "<button type='button' class='preview-comment-add'>Comment</button>"
|
|
3851
|
+
+ "</div>"
|
|
3852
|
+
+ "<div class='preview-comment-block-content preview-code-line-content'>" + lineHtml + "</div>"
|
|
3853
|
+
+ "</div>",
|
|
3854
|
+
);
|
|
3855
|
+
|
|
3856
|
+
offset = end + 1;
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
return "<div class='response-markdown-highlight preview-code-lines'>" + html.join("") + "</div>";
|
|
3860
|
+
}
|
|
3861
|
+
|
|
3862
|
+
function renderCodePreviewWithCommentBlocks(targetEl, text, pane) {
|
|
3863
|
+
if (!targetEl) return;
|
|
3864
|
+
clearPreviewJumpHighlight(targetEl);
|
|
3865
|
+
finishPreviewRender(targetEl);
|
|
3866
|
+
targetEl.innerHTML = buildCodePreviewHtmlWithCommentBlocks(text, editorLanguage || "");
|
|
3867
|
+
updatePreviewCommentBlocksForElement(targetEl);
|
|
3868
|
+
if (pane === "response") {
|
|
3869
|
+
applyPendingResponseScrollReset();
|
|
3870
|
+
scheduleResponsePaneRepaintNudge();
|
|
3871
|
+
}
|
|
3872
|
+
}
|
|
3873
|
+
|
|
3798
3874
|
function detectLanguageFromName(name) {
|
|
3799
3875
|
if (!name) return "";
|
|
3800
3876
|
var dot = name.lastIndexOf(".");
|
|
@@ -4358,7 +4434,10 @@
|
|
|
4358
4434
|
}
|
|
4359
4435
|
|
|
4360
4436
|
function supportsPreviewCommentsForCurrentEditor() {
|
|
4361
|
-
|
|
4437
|
+
// LaTeX preview comments are intentionally disabled for now.
|
|
4438
|
+
// The initial client-side source/block heuristics were too unreliable on complex real documents.
|
|
4439
|
+
// Keep the independent LaTeX rendering fixes, but only enable preview comments where mapping is robust.
|
|
4440
|
+
return editorLanguage === "markdown" || supportsCodePreviewCommentsForCurrentEditor();
|
|
4362
4441
|
}
|
|
4363
4442
|
|
|
4364
4443
|
function getPreviewCommentBlockKindLabel(kind) {
|
|
@@ -4367,11 +4446,20 @@
|
|
|
4367
4446
|
if (kind === "list") return "list";
|
|
4368
4447
|
if (kind === "code") return "code block";
|
|
4369
4448
|
if (kind === "table") return "table";
|
|
4449
|
+
if (kind === "code-line") return "code line";
|
|
4450
|
+
if (kind === "diff-line") return "diff line";
|
|
4451
|
+
if (kind === "text-line") return "text line";
|
|
4370
4452
|
return "paragraph";
|
|
4371
4453
|
}
|
|
4372
4454
|
|
|
4373
4455
|
function supportsPreviewSelectionCommentsForBlockKind(kind) {
|
|
4374
|
-
return kind === "paragraph"
|
|
4456
|
+
return kind === "paragraph"
|
|
4457
|
+
|| kind === "heading"
|
|
4458
|
+
|| kind === "blockquote"
|
|
4459
|
+
|| kind === "list"
|
|
4460
|
+
|| kind === "code-line"
|
|
4461
|
+
|| kind === "diff-line"
|
|
4462
|
+
|| kind === "text-line";
|
|
4375
4463
|
}
|
|
4376
4464
|
|
|
4377
4465
|
function normalizeVisiblePreviewText(text) {
|
|
@@ -4708,6 +4796,11 @@
|
|
|
4708
4796
|
return safeStartA < safeEndB && safeStartB < safeEndA;
|
|
4709
4797
|
}
|
|
4710
4798
|
|
|
4799
|
+
function scanSourcePreviewCommentBlocks(markdown) {
|
|
4800
|
+
if (editorLanguage !== "markdown") return [];
|
|
4801
|
+
return scanMarkdownPreviewCommentBlocks(markdown);
|
|
4802
|
+
}
|
|
4803
|
+
|
|
4711
4804
|
function scanMarkdownPreviewCommentBlocks(markdown) {
|
|
4712
4805
|
const source = String(markdown || "").replace(/\r\n/g, "\n");
|
|
4713
4806
|
const lines = source.split("\n");
|
|
@@ -5074,7 +5167,7 @@
|
|
|
5074
5167
|
|
|
5075
5168
|
function decorateRenderedEditorPreviewComments(targetEl, sourceText) {
|
|
5076
5169
|
if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
|
|
5077
|
-
const sourceBlocks =
|
|
5170
|
+
const sourceBlocks = scanSourcePreviewCommentBlocks(sourceText);
|
|
5078
5171
|
const targetBlocks = collectPreviewCommentTargetElements(targetEl);
|
|
5079
5172
|
if (sourceBlocks.length === 0 || targetBlocks.length === 0) return;
|
|
5080
5173
|
|
|
@@ -5268,12 +5361,43 @@
|
|
|
5268
5361
|
return bestBlock;
|
|
5269
5362
|
}
|
|
5270
5363
|
|
|
5364
|
+
function getPreviewNoteNormalizedSelectionText(note) {
|
|
5365
|
+
const direct = normalizeVisiblePreviewText(note && (note.selectedDisplayText || note.selectedText) ? (note.selectedDisplayText || note.selectedText) : "");
|
|
5366
|
+
if (direct) return direct;
|
|
5367
|
+
return "";
|
|
5368
|
+
}
|
|
5369
|
+
|
|
5370
|
+
function findPreviewCommentBlockForNoteText(targetEl, note) {
|
|
5371
|
+
if (!targetEl || !note || typeof targetEl.querySelectorAll !== "function") return null;
|
|
5372
|
+
const selectionText = getPreviewNoteNormalizedSelectionText(note);
|
|
5373
|
+
if (!selectionText) return null;
|
|
5374
|
+
|
|
5375
|
+
let bestBlock = null;
|
|
5376
|
+
let bestScore = Number.NEGATIVE_INFINITY;
|
|
5377
|
+
Array.from(targetEl.querySelectorAll(".preview-comment-block")).forEach((blockEl) => {
|
|
5378
|
+
const contentEl = blockEl.querySelector(".preview-comment-block-content") || blockEl;
|
|
5379
|
+
const blockText = normalizeVisiblePreviewText(buildNormalizedDomTextMap(contentEl).text);
|
|
5380
|
+
if (!blockText) return;
|
|
5381
|
+
const matchIndex = blockText.indexOf(selectionText);
|
|
5382
|
+
if (matchIndex < 0) return;
|
|
5383
|
+
const lineStart = Math.max(1, Number(blockEl.dataset && blockEl.dataset.reviewNoteLineStart) || 1);
|
|
5384
|
+
const desiredLine = Math.max(1, Number(note && note.lineStart) || 1);
|
|
5385
|
+
const proximityPenalty = Math.abs(lineStart - desiredLine);
|
|
5386
|
+
const score = 1000000 - (matchIndex * 4) - proximityPenalty - Math.max(0, blockText.length - selectionText.length);
|
|
5387
|
+
if (score > bestScore) {
|
|
5388
|
+
bestScore = score;
|
|
5389
|
+
bestBlock = blockEl;
|
|
5390
|
+
}
|
|
5391
|
+
});
|
|
5392
|
+
return bestBlock;
|
|
5393
|
+
}
|
|
5394
|
+
|
|
5271
5395
|
function revealReviewNoteInPreviewElement(targetEl, note) {
|
|
5272
5396
|
if (!targetEl || !note) return false;
|
|
5273
5397
|
const source = String(sourceTextEl && sourceTextEl.value ? sourceTextEl.value : "");
|
|
5274
5398
|
const range = resolveReviewNoteRange(note, source);
|
|
5275
5399
|
if (!range) return false;
|
|
5276
|
-
const blockEl = findPreviewCommentBlockForRange(targetEl, range);
|
|
5400
|
+
const blockEl = findPreviewCommentBlockForRange(targetEl, range) || findPreviewCommentBlockForNoteText(targetEl, note);
|
|
5277
5401
|
if (!blockEl) return false;
|
|
5278
5402
|
const contentEl = blockEl.querySelector(".preview-comment-block-content") || blockEl;
|
|
5279
5403
|
const inlineHighlightEl = createPreviewJumpInlineHighlight(contentEl, blockEl, note, range);
|
|
@@ -5285,6 +5409,7 @@
|
|
|
5285
5409
|
}
|
|
5286
5410
|
|
|
5287
5411
|
function revealReviewNoteInPreview(note) {
|
|
5412
|
+
if (!supportsPreviewCommentsForCurrentEditor()) return;
|
|
5288
5413
|
if (rightView === "editor-preview" && critiqueViewEl && critiqueViewEl.isConnected) {
|
|
5289
5414
|
revealReviewNoteInPreviewElement(critiqueViewEl, note);
|
|
5290
5415
|
}
|
|
@@ -5407,7 +5532,8 @@
|
|
|
5407
5532
|
function updateEditorSelectionCommentUi() {
|
|
5408
5533
|
if (!editorSelectionCommentBtn) return;
|
|
5409
5534
|
const hasSelection = Boolean(
|
|
5410
|
-
|
|
5535
|
+
!suppressEditorSelectionComment
|
|
5536
|
+
&& editorView === "markdown"
|
|
5411
5537
|
&& document.activeElement === sourceTextEl
|
|
5412
5538
|
&& typeof sourceTextEl.selectionStart === "number"
|
|
5413
5539
|
&& typeof sourceTextEl.selectionEnd === "number"
|
|
@@ -5420,6 +5546,14 @@
|
|
|
5420
5546
|
}
|
|
5421
5547
|
}
|
|
5422
5548
|
|
|
5549
|
+
function clearSuppressedEditorSelectionComment() {
|
|
5550
|
+
if (!suppressEditorSelectionComment) return;
|
|
5551
|
+
suppressEditorSelectionComment = false;
|
|
5552
|
+
suppressedEditorSelectionStart = null;
|
|
5553
|
+
suppressedEditorSelectionEnd = null;
|
|
5554
|
+
updateEditorSelectionCommentUi();
|
|
5555
|
+
}
|
|
5556
|
+
|
|
5423
5557
|
function updateReviewNotesUi() {
|
|
5424
5558
|
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
5425
5559
|
const count = reviewNotes.length;
|
|
@@ -5667,9 +5801,11 @@
|
|
|
5667
5801
|
if (editorSelectionCommentBtn) {
|
|
5668
5802
|
editorSelectionCommentBtn.hidden = true;
|
|
5669
5803
|
}
|
|
5804
|
+
const shouldOpenReviewNotes = !isReviewNotesOpen();
|
|
5670
5805
|
pendingReviewNoteFocusId = note.id;
|
|
5671
5806
|
setReviewNotes(reviewNotes.concat([note]));
|
|
5672
|
-
if (
|
|
5807
|
+
if (shouldOpenReviewNotes) {
|
|
5808
|
+
pendingReviewNoteFocusId = note.id;
|
|
5673
5809
|
openReviewNotes();
|
|
5674
5810
|
}
|
|
5675
5811
|
const schedule = typeof window.requestAnimationFrame === "function"
|
|
@@ -5713,6 +5849,10 @@
|
|
|
5713
5849
|
setStatus("Could not find the anchored location for this comment.", "warning");
|
|
5714
5850
|
return;
|
|
5715
5851
|
}
|
|
5852
|
+
suppressEditorSelectionComment = true;
|
|
5853
|
+
suppressedEditorSelectionStart = range.start;
|
|
5854
|
+
suppressedEditorSelectionEnd = range.end;
|
|
5855
|
+
updateEditorSelectionCommentUi();
|
|
5716
5856
|
setEditorView("markdown");
|
|
5717
5857
|
setActivePane("left");
|
|
5718
5858
|
sourceTextEl.focus();
|
|
@@ -5723,6 +5863,7 @@
|
|
|
5723
5863
|
schedule(() => {
|
|
5724
5864
|
scrollEditorRangeIntoView(range);
|
|
5725
5865
|
revealReviewNoteInPreview(note);
|
|
5866
|
+
updateEditorSelectionCommentUi();
|
|
5726
5867
|
});
|
|
5727
5868
|
}
|
|
5728
5869
|
|
|
@@ -7218,6 +7359,7 @@
|
|
|
7218
7359
|
if (activePreviewCommentSelection) {
|
|
7219
7360
|
clearPreviewCommentSelection();
|
|
7220
7361
|
}
|
|
7362
|
+
clearSuppressedEditorSelectionComment();
|
|
7221
7363
|
renderSourcePreview({ previewDelayMs: PREVIEW_INPUT_DEBOUNCE_MS });
|
|
7222
7364
|
scheduleEditorMetaUpdate();
|
|
7223
7365
|
updateEditorSelectionCommentUi();
|
|
@@ -7228,6 +7370,14 @@
|
|
|
7228
7370
|
});
|
|
7229
7371
|
|
|
7230
7372
|
sourceTextEl.addEventListener("select", () => {
|
|
7373
|
+
if (suppressEditorSelectionComment) {
|
|
7374
|
+
const selectionStart = typeof sourceTextEl.selectionStart === "number" ? sourceTextEl.selectionStart : 0;
|
|
7375
|
+
const selectionEnd = typeof sourceTextEl.selectionEnd === "number" ? sourceTextEl.selectionEnd : selectionStart;
|
|
7376
|
+
const matchesSuppressedSelection = selectionStart === suppressedEditorSelectionStart && selectionEnd === suppressedEditorSelectionEnd;
|
|
7377
|
+
if (!matchesSuppressedSelection && selectionEnd > selectionStart) {
|
|
7378
|
+
clearSuppressedEditorSelectionComment();
|
|
7379
|
+
}
|
|
7380
|
+
}
|
|
7231
7381
|
updateEditorSelectionCommentUi();
|
|
7232
7382
|
});
|
|
7233
7383
|
|
|
@@ -7661,7 +7811,7 @@
|
|
|
7661
7811
|
event.preventDefault();
|
|
7662
7812
|
event.stopPropagation();
|
|
7663
7813
|
const mode = String(actionBtn.dataset && actionBtn.dataset.previewCommentMode ? actionBtn.dataset.previewCommentMode : "");
|
|
7664
|
-
if (mode
|
|
7814
|
+
if (!mode || !mode.startsWith("selection")) return;
|
|
7665
7815
|
addReviewNoteFromPreviewSelection(blockEl);
|
|
7666
7816
|
}
|
|
7667
7817
|
|
package/client/studio.css
CHANGED
|
@@ -898,6 +898,24 @@
|
|
|
898
898
|
scroll-margin-top: 24px;
|
|
899
899
|
}
|
|
900
900
|
|
|
901
|
+
.preview-comment-line-block {
|
|
902
|
+
min-height: 1.5em;
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
.preview-code-lines {
|
|
906
|
+
white-space: pre-wrap;
|
|
907
|
+
word-break: break-word;
|
|
908
|
+
overflow-wrap: anywhere;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.preview-code-line-content {
|
|
912
|
+
display: block;
|
|
913
|
+
min-height: 1.5em;
|
|
914
|
+
white-space: inherit;
|
|
915
|
+
word-break: inherit;
|
|
916
|
+
overflow-wrap: inherit;
|
|
917
|
+
}
|
|
918
|
+
|
|
901
919
|
.preview-comment-controls {
|
|
902
920
|
position: absolute;
|
|
903
921
|
top: 0;
|
package/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { basename, dirname, extname, isAbsolute, join, resolve } from "node:path
|
|
|
10
10
|
import { URL, pathToFileURL } from "node:url";
|
|
11
11
|
import { WebSocketServer, WebSocket, type RawData } from "ws";
|
|
12
12
|
import {
|
|
13
|
+
advancePastStudioInlineBacktickSpan,
|
|
13
14
|
collectStudioInlineAnnotationMarkers,
|
|
14
15
|
hasStudioMarkdownAnnotationMarkers,
|
|
15
16
|
isStudioAnnotationWordChar,
|
|
@@ -3263,6 +3264,26 @@ function inferStudioPdfLanguage(markdown: string, editorLanguage?: string): stri
|
|
|
3263
3264
|
return undefined;
|
|
3264
3265
|
}
|
|
3265
3266
|
|
|
3267
|
+
function stripStudioMarkdownInlineCodeSpans(markdown: string): string {
|
|
3268
|
+
const source = String(markdown ?? "");
|
|
3269
|
+
let out = "";
|
|
3270
|
+
let index = 0;
|
|
3271
|
+
while (index < source.length) {
|
|
3272
|
+
if (source[index] === "`") {
|
|
3273
|
+
index = advancePastStudioInlineBacktickSpan(source, index);
|
|
3274
|
+
continue;
|
|
3275
|
+
}
|
|
3276
|
+
out += source[index];
|
|
3277
|
+
index += 1;
|
|
3278
|
+
}
|
|
3279
|
+
return out;
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
function isLikelyStandaloneLatexPreview(markdown: string): boolean {
|
|
3283
|
+
const outsideFences = transformStudioMarkdownOutsideFences(markdown, (segment: string) => stripStudioMarkdownInlineCodeSpans(segment));
|
|
3284
|
+
return /\\documentclass\b|\\begin\{document\}/.test(outsideFences);
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3266
3287
|
function escapeStudioPdfLatexText(text: string): string {
|
|
3267
3288
|
const normalized = String(text ?? "")
|
|
3268
3289
|
.replace(/\r\n/g, "\n")
|
|
@@ -4043,9 +4064,15 @@ function prepareStudioPdfMarkdown(markdown: string, isLatex?: boolean, editorLan
|
|
|
4043
4064
|
}
|
|
4044
4065
|
|
|
4045
4066
|
function stripMathMlAnnotationTags(html: string): string {
|
|
4046
|
-
return html
|
|
4047
|
-
.
|
|
4048
|
-
|
|
4067
|
+
return String(html ?? "").replace(/<math\b([^>]*)>([\s\S]*?)<\/math>/gi, (_match, attrs, inner) => {
|
|
4068
|
+
const texAnnotationMatch = String(inner ?? "").match(/<annotation\b[^>]*encoding="application\/x-tex"[^>]*>([\s\S]*?)<\/annotation>/i);
|
|
4069
|
+
const texSource = texAnnotationMatch ? String(texAnnotationMatch[1] ?? "").trim() : "";
|
|
4070
|
+
const cleanedInner = String(inner ?? "")
|
|
4071
|
+
.replace(/<annotation-xml\b[\s\S]*?<\/annotation-xml>/gi, "")
|
|
4072
|
+
.replace(/<annotation\b[\s\S]*?<\/annotation>/gi, "");
|
|
4073
|
+
const texAttr = texSource ? ` data-tex-source="${escapeStudioHtmlText(texSource)}"` : "";
|
|
4074
|
+
return `<math${attrs}${texAttr}>${cleanedInner}</math>`;
|
|
4075
|
+
});
|
|
4049
4076
|
}
|
|
4050
4077
|
|
|
4051
4078
|
function normalizeObsidianImages(markdown: string): string {
|
|
@@ -7869,8 +7896,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
7869
7896
|
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { resourceDir?: unknown }).resourceDir === "string"
|
|
7870
7897
|
? (parsedBody as { resourceDir: string }).resourceDir
|
|
7871
7898
|
: "";
|
|
7899
|
+
const requestedEditorLanguage =
|
|
7900
|
+
parsedBody && typeof parsedBody === "object" && typeof (parsedBody as { editorLanguage?: unknown }).editorLanguage === "string"
|
|
7901
|
+
? (parsedBody as { editorLanguage: string }).editorLanguage
|
|
7902
|
+
: "";
|
|
7872
7903
|
const resourcePath = resolveStudioBaseDir(sourcePath || undefined, userResourceDir || undefined, studioCwd);
|
|
7873
|
-
const
|
|
7904
|
+
const editorPreviewLanguage = normalizeStudioEditorLanguage(requestedEditorLanguage);
|
|
7905
|
+
const isLatex = editorPreviewLanguage === "latex"
|
|
7906
|
+
|| (
|
|
7907
|
+
(editorPreviewLanguage === undefined || editorPreviewLanguage === "markdown")
|
|
7908
|
+
&& isLikelyStandaloneLatexPreview(markdown)
|
|
7909
|
+
);
|
|
7874
7910
|
const html = await renderStudioMarkdownWithPandoc(markdown, isLatex, resourcePath, sourcePath || undefined);
|
|
7875
7911
|
respondJson(res, 200, { ok: true, html, renderer: "pandoc" });
|
|
7876
7912
|
} catch (error) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-studio",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.48",
|
|
4
4
|
"description": "Two-pane browser workspace for pi with prompt/response editing, annotations, critiques, prompt/response history, and live Markdown/LaTeX/code preview",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|