pi-studio 0.5.11 → 0.5.13

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/index.ts +412 -25
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,18 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.13] — 2026-03-15
8
+
9
+ ### Fixed
10
+ - Studio `Editor (Preview)` PDF export now fences non-markdown editor content such as diff/code before Pandoc export, preventing LaTeX failures on raw diff/code text.
11
+ - Non-markdown editor preview modes such as `diff` now support inline `[an: ...]` markers and render them as compact note pills.
12
+ - The editor highlight overlay keeps exact annotation source text/width, preserving cursor and text alignment while preview-only panes use the compact annotation-pill rendering.
13
+
14
+ ## [0.5.12] — 2026-03-15
15
+
16
+ ### Added
17
+ - Studio now has a `Load git diff` button that loads the current git changes (staged + unstaged tracked changes plus untracked text files) into the editor from the current Studio context and sets the editor language to `diff`.
18
+
7
19
  ## [0.5.11] — 2026-03-15
8
20
 
9
21
  ### Added
package/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext, SessionEntry, Theme } from "@mariozechner/pi-coding-agent";
2
- import { spawn } from "node:child_process";
2
+ import { spawn, spawnSync } from "node:child_process";
3
3
  import { randomUUID } from "node:crypto";
4
4
  import { readFileSync, statSync, writeFileSync } from "node:fs";
5
5
  import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
@@ -120,6 +120,13 @@ interface GetFromEditorRequestMessage {
120
120
  requestId: string;
121
121
  }
122
122
 
123
+ interface LoadGitDiffRequestMessage {
124
+ type: "load_git_diff_request";
125
+ requestId: string;
126
+ sourcePath?: string;
127
+ resourceDir?: string;
128
+ }
129
+
123
130
  interface CancelRequestMessage {
124
131
  type: "cancel_request";
125
132
  requestId: string;
@@ -137,6 +144,7 @@ type IncomingStudioMessage =
137
144
  | SaveOverRequestMessage
138
145
  | SendToEditorRequestMessage
139
146
  | GetFromEditorRequestMessage
147
+ | LoadGitDiffRequestMessage
140
148
  | CancelRequestMessage;
141
149
 
142
150
  const REQUEST_TIMEOUT_MS = 5 * 60 * 1000;
@@ -847,7 +855,9 @@ function readStudioFile(pathArg: string, cwd: string):
847
855
  | { ok: true; text: string; label: string; resolvedPath: string }
848
856
  | { ok: false; message: string } {
849
857
  const resolved = resolveStudioPath(pathArg, cwd);
850
- if (!resolved.ok) return resolved;
858
+ if (resolved.ok === false) {
859
+ return { ok: false, message: resolved.message };
860
+ }
851
861
 
852
862
  try {
853
863
  const stats = statSync(resolved.resolved);
@@ -891,7 +901,9 @@ function writeStudioFile(pathArg: string, cwd: string, content: string):
891
901
  | { ok: true; label: string; resolvedPath: string }
892
902
  | { ok: false; message: string } {
893
903
  const resolved = resolveStudioPath(pathArg, cwd);
894
- if (!resolved.ok) return resolved;
904
+ if (resolved.ok === false) {
905
+ return { ok: false, message: resolved.message };
906
+ }
895
907
 
896
908
  try {
897
909
  writeFileSync(resolved.resolved, content, "utf-8");
@@ -904,6 +916,207 @@ function writeStudioFile(pathArg: string, cwd: string, content: string):
904
916
  }
905
917
  }
906
918
 
919
+ function splitStudioGitPathOutput(output: string): string[] {
920
+ return output
921
+ .split(/\r?\n/)
922
+ .map((line) => line.trim())
923
+ .filter((line) => line.length > 0);
924
+ }
925
+
926
+ function formatStudioGitSpawnFailure(
927
+ result: { stdout?: string | Buffer | null; stderr?: string | Buffer | null },
928
+ args: string[],
929
+ ): string {
930
+ const stderr = typeof result.stderr === "string"
931
+ ? result.stderr.trim()
932
+ : (result.stderr ? result.stderr.toString("utf-8").trim() : "");
933
+ const stdout = typeof result.stdout === "string"
934
+ ? result.stdout.trim()
935
+ : (result.stdout ? result.stdout.toString("utf-8").trim() : "");
936
+ return stderr || stdout || `git ${args.join(" ")} failed`;
937
+ }
938
+
939
+ function readStudioTextFileIfPossible(path: string): string | null {
940
+ try {
941
+ const buf = readFileSync(path);
942
+ const sample = buf.subarray(0, 8192);
943
+ let nulCount = 0;
944
+ let controlCount = 0;
945
+ for (let i = 0; i < sample.length; i++) {
946
+ const b = sample[i];
947
+ if (b === 0x00) nulCount += 1;
948
+ else if (b < 0x08 || (b > 0x0D && b < 0x20 && b !== 0x1B)) controlCount += 1;
949
+ }
950
+ if (nulCount > 0 || (sample.length > 0 && controlCount / sample.length > 0.1)) {
951
+ return null;
952
+ }
953
+ return buf.toString("utf-8").replace(/\r\n/g, "\n");
954
+ } catch {
955
+ return null;
956
+ }
957
+ }
958
+
959
+ function buildStudioSyntheticNewFileDiff(filePath: string, content: string): string {
960
+ const lines = content.split("\n");
961
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
962
+ lines.pop();
963
+ }
964
+
965
+ const diffLines = [
966
+ `diff --git a/${filePath} b/${filePath}`,
967
+ "new file mode 100644",
968
+ "--- /dev/null",
969
+ `+++ b/${filePath}`,
970
+ `@@ -0,0 +1,${lines.length} @@`,
971
+ ];
972
+
973
+ if (lines.length > 0) {
974
+ diffLines.push(lines.map((line) => `+${line}`).join("\n"));
975
+ }
976
+
977
+ return diffLines.join("\n");
978
+ }
979
+
980
+ function resolveStudioGitDiffBaseDir(sourcePath: string | undefined, resourceDir: string | undefined, fallbackCwd: string): string {
981
+ const source = typeof sourcePath === "string" ? sourcePath.trim() : "";
982
+ if (source) {
983
+ return dirname(source);
984
+ }
985
+
986
+ const resource = typeof resourceDir === "string" ? resourceDir.trim() : "";
987
+ if (resource) {
988
+ return isAbsolute(resource) ? resource : resolve(fallbackCwd, resource);
989
+ }
990
+
991
+ return fallbackCwd;
992
+ }
993
+
994
+ function readStudioGitDiff(baseDir: string):
995
+ | { ok: true; text: string; label: string }
996
+ | { ok: false; level: "info" | "warning" | "error"; message: string } {
997
+ const repoRootArgs = ["rev-parse", "--show-toplevel"];
998
+ const repoRootResult = spawnSync("git", repoRootArgs, {
999
+ cwd: baseDir,
1000
+ encoding: "utf-8",
1001
+ });
1002
+ if (repoRootResult.status !== 0) {
1003
+ return {
1004
+ ok: false,
1005
+ level: "warning",
1006
+ message: "No git repository found for the current Studio context.",
1007
+ };
1008
+ }
1009
+ const repoRoot = repoRootResult.stdout.trim();
1010
+
1011
+ const hasHead = spawnSync("git", ["rev-parse", "--verify", "HEAD"], {
1012
+ cwd: repoRoot,
1013
+ encoding: "utf-8",
1014
+ }).status === 0;
1015
+
1016
+ const untrackedArgs = ["ls-files", "--others", "--exclude-standard"];
1017
+ const untrackedResult = spawnSync("git", untrackedArgs, {
1018
+ cwd: repoRoot,
1019
+ encoding: "utf-8",
1020
+ });
1021
+ if (untrackedResult.status !== 0) {
1022
+ return {
1023
+ ok: false,
1024
+ level: "error",
1025
+ message: `Failed to list untracked files: ${formatStudioGitSpawnFailure(untrackedResult, untrackedArgs)}`,
1026
+ };
1027
+ }
1028
+ const untrackedPaths = splitStudioGitPathOutput(untrackedResult.stdout ?? "").sort();
1029
+
1030
+ let diffOutput = "";
1031
+ let statSummary = "";
1032
+ let currentTreeFileCount = 0;
1033
+
1034
+ if (hasHead) {
1035
+ const diffArgs = ["diff", "HEAD", "--unified=3", "--find-renames", "--no-color", "--"];
1036
+ const diffResult = spawnSync("git", diffArgs, {
1037
+ cwd: repoRoot,
1038
+ encoding: "utf-8",
1039
+ });
1040
+ if (diffResult.status !== 0) {
1041
+ return {
1042
+ ok: false,
1043
+ level: "error",
1044
+ message: `Failed to collect git diff: ${formatStudioGitSpawnFailure(diffResult, diffArgs)}`,
1045
+ };
1046
+ }
1047
+ diffOutput = diffResult.stdout ?? "";
1048
+
1049
+ const statArgs = ["diff", "HEAD", "--stat", "--find-renames", "--no-color", "--"];
1050
+ const statResult = spawnSync("git", statArgs, {
1051
+ cwd: repoRoot,
1052
+ encoding: "utf-8",
1053
+ });
1054
+ if (statResult.status === 0) {
1055
+ const statLines = splitStudioGitPathOutput(statResult.stdout ?? "");
1056
+ statSummary = statLines.length > 0 ? (statLines[statLines.length - 1] ?? "") : "";
1057
+ }
1058
+ } else {
1059
+ const trackedArgs = ["ls-files", "--cached"];
1060
+ const trackedResult = spawnSync("git", trackedArgs, {
1061
+ cwd: repoRoot,
1062
+ encoding: "utf-8",
1063
+ });
1064
+ if (trackedResult.status !== 0) {
1065
+ return {
1066
+ ok: false,
1067
+ level: "error",
1068
+ message: `Failed to inspect tracked files: ${formatStudioGitSpawnFailure(trackedResult, trackedArgs)}`,
1069
+ };
1070
+ }
1071
+
1072
+ const trackedPaths = splitStudioGitPathOutput(trackedResult.stdout ?? "");
1073
+ const currentTreePaths = Array.from(new Set([...trackedPaths, ...untrackedPaths])).sort();
1074
+ currentTreeFileCount = currentTreePaths.length;
1075
+ diffOutput = currentTreePaths
1076
+ .map((filePath) => {
1077
+ const content = readStudioTextFileIfPossible(join(repoRoot, filePath));
1078
+ if (content == null) return "";
1079
+ return buildStudioSyntheticNewFileDiff(filePath, content);
1080
+ })
1081
+ .filter((section) => section.length > 0)
1082
+ .join("\n\n");
1083
+ }
1084
+
1085
+ const untrackedSections = hasHead
1086
+ ? untrackedPaths
1087
+ .map((filePath) => {
1088
+ const content = readStudioTextFileIfPossible(join(repoRoot, filePath));
1089
+ if (content == null) return "";
1090
+ return buildStudioSyntheticNewFileDiff(filePath, content);
1091
+ })
1092
+ .filter((section) => section.length > 0)
1093
+ : [];
1094
+
1095
+ const fullDiff = [diffOutput.trimEnd(), ...untrackedSections].filter(Boolean).join("\n\n");
1096
+ if (!fullDiff.trim()) {
1097
+ return {
1098
+ ok: false,
1099
+ level: "info",
1100
+ message: "No uncommitted git changes to load.",
1101
+ };
1102
+ }
1103
+
1104
+ const summaryParts: string[] = [];
1105
+ if (hasHead && statSummary) {
1106
+ summaryParts.push(statSummary);
1107
+ }
1108
+ if (!hasHead && currentTreeFileCount > 0) {
1109
+ summaryParts.push(`${currentTreeFileCount} file${currentTreeFileCount === 1 ? "" : "s"} in current tree`);
1110
+ }
1111
+ if (untrackedPaths.length > 0) {
1112
+ summaryParts.push(`${untrackedPaths.length} untracked file${untrackedPaths.length === 1 ? "" : "s"}`);
1113
+ }
1114
+
1115
+ const labelBase = hasHead ? "git diff HEAD" : "git diff (no commits yet)";
1116
+ const label = summaryParts.length > 0 ? `${labelBase} (${summaryParts.join(", ")})` : labelBase;
1117
+ return { ok: true, text: fullDiff, label };
1118
+ }
1119
+
907
1120
  function readLocalPackageMetadata(): { name: string; version: string } | null {
908
1121
  try {
909
1122
  const raw = readFileSync(new URL("./package.json", import.meta.url), "utf-8");
@@ -1904,6 +2117,20 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
1904
2117
  };
1905
2118
  }
1906
2119
 
2120
+ if (
2121
+ msg.type === "load_git_diff_request"
2122
+ && typeof msg.requestId === "string"
2123
+ && (msg.sourcePath === undefined || typeof msg.sourcePath === "string")
2124
+ && (msg.resourceDir === undefined || typeof msg.resourceDir === "string")
2125
+ ) {
2126
+ return {
2127
+ type: "load_git_diff_request",
2128
+ requestId: msg.requestId,
2129
+ sourcePath: typeof msg.sourcePath === "string" ? msg.sourcePath : undefined,
2130
+ resourceDir: typeof msg.resourceDir === "string" ? msg.resourceDir : undefined,
2131
+ };
2132
+ }
2133
+
1907
2134
  if (msg.type === "cancel_request" && typeof msg.requestId === "string") {
1908
2135
  return {
1909
2136
  type: "cancel_request",
@@ -3258,6 +3485,8 @@ ${cssVarsBlock}
3258
3485
  <button id="saveAsBtn" type="button" title="Save editor content to a new file path.">Save editor as…</button>
3259
3486
  <button id="saveOverBtn" type="button" title="Overwrite current file with editor content." disabled>Save editor</button>
3260
3487
  <label class="file-label" title="Load a local file into editor text.">Load file content<input id="fileInput" type="file" accept=".md,.markdown,.mdx,.js,.mjs,.cjs,.jsx,.ts,.mts,.cts,.tsx,.py,.pyw,.sh,.bash,.zsh,.json,.jsonc,.json5,.rs,.c,.h,.cpp,.cxx,.cc,.hpp,.hxx,.jl,.f90,.f95,.f03,.f,.for,.r,.R,.m,.tex,.latex,.diff,.patch,.java,.go,.rb,.swift,.html,.htm,.css,.xml,.yaml,.yml,.toml,.lua,.txt,.rst,.adoc" /></label>
3488
+ <button id="loadGitDiffBtn" type="button" title="Load the current git diff from the Studio context into the editor.">Load git diff</button>
3489
+ <button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
3261
3490
  </div>
3262
3491
  </header>
3263
3492
 
@@ -3286,7 +3515,6 @@ ${cssVarsBlock}
3286
3515
  <button id="sendRunBtn" type="button" title="Send editor text directly to the model as-is. Shortcut: Cmd/Ctrl+Enter when editor pane is active.">Run editor text</button>
3287
3516
  <button id="copyDraftBtn" type="button">Copy editor text</button>
3288
3517
  <button id="sendEditorBtn" type="button">Send to pi editor</button>
3289
- <button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
3290
3518
  </div>
3291
3519
  <div class="source-actions-row">
3292
3520
  <button id="insertHeaderBtn" type="button" title="Insert annotated-reply protocol header (source metadata, [an: ...] syntax hint, precedence note, and end marker).">Insert annotated reply header</button>
@@ -3483,6 +3711,7 @@ ${cssVarsBlock}
3483
3711
  const saveOverBtn = document.getElementById("saveOverBtn");
3484
3712
  const sendEditorBtn = document.getElementById("sendEditorBtn");
3485
3713
  const getEditorBtn = document.getElementById("getEditorBtn");
3714
+ const loadGitDiffBtn = document.getElementById("loadGitDiffBtn");
3486
3715
  const sendRunBtn = document.getElementById("sendRunBtn");
3487
3716
  const copyDraftBtn = document.getElementById("copyDraftBtn");
3488
3717
  const saveAnnotatedBtn = document.getElementById("saveAnnotatedBtn");
@@ -3773,6 +4002,7 @@ ${cssVarsBlock}
3773
4002
  if (kind === "compact") return "compacting context";
3774
4003
  if (kind === "send_to_editor") return "sending to pi editor";
3775
4004
  if (kind === "get_from_editor") return "loading from pi editor";
4005
+ if (kind === "load_git_diff") return "loading git diff";
3776
4006
  if (kind === "save_as" || kind === "save_over") return "saving editor text";
3777
4007
  return "submitting request";
3778
4008
  }
@@ -4436,6 +4666,24 @@ ${cssVarsBlock}
4436
4666
  return annotationsEnabled ? raw : stripAnnotationMarkers(raw);
4437
4667
  }
4438
4668
 
4669
+ function wrapAsFencedCodeBlock(text, language) {
4670
+ const source = String(text || "").trimEnd();
4671
+ const lang = String(language || "").trim();
4672
+ const backtickFence = "\x60\x60\x60";
4673
+ const newline = "\\n";
4674
+ const marker = source.includes(backtickFence) ? "~~~" : backtickFence;
4675
+ return marker + (lang ? lang : "") + newline + source + newline + marker;
4676
+ }
4677
+
4678
+ function prepareEditorTextForPdfExport(text) {
4679
+ const prepared = prepareEditorTextForPreview(text);
4680
+ const lang = normalizeFenceLanguage(editorLanguage || "");
4681
+ if (lang && lang !== "markdown" && lang !== "latex") {
4682
+ return wrapAsFencedCodeBlock(prepared, lang);
4683
+ }
4684
+ return prepared;
4685
+ }
4686
+
4439
4687
  function updateSyncBadge(normalizedEditorText) {
4440
4688
  if (!syncBadgeEl) return;
4441
4689
 
@@ -4794,7 +5042,7 @@ ${cssVarsBlock}
4794
5042
  return;
4795
5043
  }
4796
5044
 
4797
- const markdown = rightView === "editor-preview" ? prepareEditorTextForPreview(sourceTextEl.value) : latestResponseMarkdown;
5045
+ const markdown = rightView === "editor-preview" ? prepareEditorTextForPdfExport(sourceTextEl.value) : latestResponseMarkdown;
4798
5046
  if (!markdown || !markdown.trim()) {
4799
5047
  setStatus("Nothing to export yet.", "warning");
4800
5048
  return;
@@ -4802,7 +5050,8 @@ ${cssVarsBlock}
4802
5050
 
4803
5051
  const sourcePath = sourceState.path || "";
4804
5052
  const resourceDir = (!sourceState.path && resourceDirInput) ? resourceDirInput.value.trim() : "";
4805
- const isLatex = /\\\\documentclass\\b|\\\\begin\\{document\\}/.test(markdown);
5053
+ const editorPdfLanguage = rightView === "editor-preview" ? normalizeFenceLanguage(editorLanguage || "") : "";
5054
+ const isLatex = editorPdfLanguage === "latex" || /\\\\documentclass\\b|\\\\begin\\{document\\}/.test(markdown);
4806
5055
  let filenameHint = rightView === "editor-preview" ? "studio-editor-preview.pdf" : "studio-response-preview.pdf";
4807
5056
  if (sourceState.path) {
4808
5057
  const baseName = sourceState.path.split(/[\\\\/]/).pop() || "studio";
@@ -4924,7 +5173,7 @@ ${cssVarsBlock}
4924
5173
  const text = prepareEditorTextForPreview(sourceTextEl.value || "");
4925
5174
  if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
4926
5175
  finishPreviewRender(sourcePreviewEl);
4927
- sourcePreviewEl.innerHTML = "<div class='response-markdown-highlight' style='white-space:pre;font-family:var(--font-mono);font-size:13px;line-height:1.5;padding:16px;overflow:auto;'>" + highlightCode(text, editorLanguage) + "</div>";
5176
+ sourcePreviewEl.innerHTML = "<div class='response-markdown-highlight' style='white-space:pre;font-family:var(--font-mono);font-size:13px;line-height:1.5;padding:16px;overflow:auto;'>" + highlightCode(text, editorLanguage, "preview") + "</div>";
4928
5177
  return;
4929
5178
  }
4930
5179
  const nonce = ++sourcePreviewRenderNonce;
@@ -4989,7 +5238,7 @@ ${cssVarsBlock}
4989
5238
  }
4990
5239
  if (editorLanguage && editorLanguage !== "markdown" && editorLanguage !== "latex") {
4991
5240
  finishPreviewRender(critiqueViewEl);
4992
- critiqueViewEl.innerHTML = "<div class='response-markdown-highlight' style='white-space:pre;font-family:var(--font-mono);font-size:13px;line-height:1.5;padding:16px;overflow:auto;'>" + highlightCode(editorText, editorLanguage) + "</div>";
5241
+ critiqueViewEl.innerHTML = "<div class='response-markdown-highlight' style='white-space:pre;font-family:var(--font-mono);font-size:13px;line-height:1.5;padding:16px;overflow:auto;'>" + highlightCode(editorText, editorLanguage, "preview") + "</div>";
4993
5242
  return;
4994
5243
  }
4995
5244
  const nonce = ++responsePreviewRenderNonce;
@@ -5163,6 +5412,7 @@ ${cssVarsBlock}
5163
5412
  saveOverBtn.disabled = uiBusy || !canSaveOver;
5164
5413
  sendEditorBtn.disabled = uiBusy;
5165
5414
  if (getEditorBtn) getEditorBtn.disabled = uiBusy;
5415
+ if (loadGitDiffBtn) loadGitDiffBtn.disabled = uiBusy;
5166
5416
  syncRunAndCritiqueButtons();
5167
5417
  copyDraftBtn.disabled = uiBusy;
5168
5418
  if (highlightSelect) highlightSelect.disabled = uiBusy;
@@ -5310,6 +5560,47 @@ ${cssVarsBlock}
5310
5560
  return "<span class='" + className + "'>" + escapeHtml(String(text || "")) + "</span>";
5311
5561
  }
5312
5562
 
5563
+ function wrapHighlightWithTitle(className, text, title) {
5564
+ const titleAttr = title ? " title='" + escapeHtml(String(title)) + "'" : "";
5565
+ return "<span class='" + className + "'" + titleAttr + ">" + escapeHtml(String(text || "")) + "</span>";
5566
+ }
5567
+
5568
+ function highlightInlineAnnotations(text, mode) {
5569
+ const source = String(text || "");
5570
+ const renderMode = mode === "preview" ? "preview" : "overlay";
5571
+ ANNOTATION_MARKER_REGEX.lastIndex = 0;
5572
+ let lastIndex = 0;
5573
+ let out = "";
5574
+
5575
+ let match;
5576
+ while ((match = ANNOTATION_MARKER_REGEX.exec(source)) !== null) {
5577
+ const token = match[0] || "";
5578
+ const start = typeof match.index === "number" ? match.index : 0;
5579
+ const markerText = typeof match[1] === "string" ? match[1].trim() : token;
5580
+
5581
+ if (start > lastIndex) {
5582
+ out += escapeHtml(source.slice(lastIndex, start));
5583
+ }
5584
+
5585
+ if (renderMode === "preview") {
5586
+ out += wrapHighlightWithTitle("annotation-preview-marker", markerText || token, token);
5587
+ } else {
5588
+ out += wrapHighlight(annotationsEnabled ? "hl-annotation" : "hl-annotation-muted", token);
5589
+ }
5590
+ lastIndex = start + token.length;
5591
+ if (token.length === 0) {
5592
+ ANNOTATION_MARKER_REGEX.lastIndex += 1;
5593
+ }
5594
+ }
5595
+
5596
+ ANNOTATION_MARKER_REGEX.lastIndex = 0;
5597
+ if (lastIndex < source.length) {
5598
+ out += escapeHtml(source.slice(lastIndex));
5599
+ }
5600
+
5601
+ return out;
5602
+ }
5603
+
5313
5604
  function highlightInlineMarkdown(text) {
5314
5605
  const source = String(text || "");
5315
5606
  const pattern = /(\\x60[^\\x60]*\\x60)|(\\[[^\\]]+\\]\\([^)]+\\))|(\\[an:\\s*[^\\]]+\\])/gi;
@@ -5336,7 +5627,7 @@ ${cssVarsBlock}
5336
5627
  out += escapeHtml(token);
5337
5628
  }
5338
5629
  } else if (match[3]) {
5339
- out += wrapHighlight(annotationsEnabled ? "hl-annotation" : "hl-annotation-muted", token);
5630
+ out += highlightInlineAnnotations(token);
5340
5631
  } else {
5341
5632
  out += escapeHtml(token);
5342
5633
  }
@@ -5408,9 +5699,10 @@ ${cssVarsBlock}
5408
5699
  return out;
5409
5700
  }
5410
5701
 
5411
- function highlightCodeLine(line, language) {
5702
+ function highlightCodeLine(line, language, annotationRenderMode) {
5412
5703
  const source = String(line || "");
5413
5704
  const lang = normalizeFenceLanguage(language);
5705
+ const renderMode = annotationRenderMode === "preview" ? "preview" : "overlay";
5414
5706
 
5415
5707
  if (!lang) {
5416
5708
  return wrapHighlight("hl-code", source);
@@ -5554,14 +5846,14 @@ ${cssVarsBlock}
5554
5846
  }
5555
5847
 
5556
5848
  if (lang === "diff") {
5557
- var escaped = escapeHtml(source);
5558
- if (/^@@/.test(source)) return "<span class=\\"hl-code-fn\\">" + escaped + "</span>";
5559
- if (/^\\+\\+\\+|^---/.test(source)) return "<span class=\\"hl-code-kw\\">" + escaped + "</span>";
5560
- if (/^\\+/.test(source)) return "<span class=\\"hl-diff-add\\">" + escaped + "</span>";
5561
- if (/^-/.test(source)) return "<span class=\\"hl-diff-del\\">" + escaped + "</span>";
5562
- if (/^diff /.test(source)) return "<span class=\\"hl-code-kw\\">" + escaped + "</span>";
5563
- if (/^index /.test(source)) return "<span class=\\"hl-code-com\\">" + escaped + "</span>";
5564
- return escaped;
5849
+ var highlightedDiff = highlightInlineAnnotations(source, renderMode);
5850
+ if (/^@@/.test(source)) return "<span class=\\"hl-code-fn\\">" + highlightedDiff + "</span>";
5851
+ if (/^\\+\\+\\+|^---/.test(source)) return "<span class=\\"hl-code-kw\\">" + highlightedDiff + "</span>";
5852
+ if (/^\\+/.test(source)) return "<span class=\\"hl-diff-add\\">" + highlightedDiff + "</span>";
5853
+ if (/^-/.test(source)) return "<span class=\\"hl-diff-del\\">" + highlightedDiff + "</span>";
5854
+ if (/^diff /.test(source)) return "<span class=\\"hl-code-kw\\">" + highlightedDiff + "</span>";
5855
+ if (/^index /.test(source)) return "<span class=\\"hl-code-com\\">" + highlightedDiff + "</span>";
5856
+ return highlightedDiff;
5565
5857
  }
5566
5858
 
5567
5859
  return wrapHighlight("hl-code", source);
@@ -5637,15 +5929,16 @@ ${cssVarsBlock}
5637
5929
  return out.join("<br>");
5638
5930
  }
5639
5931
 
5640
- function highlightCode(text, language) {
5932
+ function highlightCode(text, language, annotationRenderMode) {
5641
5933
  const lines = String(text || "").replace(/\\r\\n/g, "\\n").split("\\n");
5642
5934
  const lang = normalizeFenceLanguage(language);
5935
+ const renderMode = annotationRenderMode === "preview" ? "preview" : "overlay";
5643
5936
  const out = [];
5644
5937
  for (const line of lines) {
5645
5938
  if (line.length === 0) {
5646
5939
  out.push(EMPTY_OVERLAY_LINE);
5647
5940
  } else if (lang) {
5648
- out.push(highlightCodeLine(line, lang));
5941
+ out.push(highlightCodeLine(line, lang, renderMode));
5649
5942
  } else {
5650
5943
  out.push(escapeHtml(line));
5651
5944
  }
@@ -6362,6 +6655,31 @@ ${cssVarsBlock}
6362
6655
  return;
6363
6656
  }
6364
6657
 
6658
+ if (message.type === "git_diff_snapshot") {
6659
+ if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
6660
+ pendingRequestId = null;
6661
+ pendingKind = null;
6662
+ }
6663
+
6664
+ const content = typeof message.content === "string" ? message.content : "";
6665
+ const label = typeof message.label === "string" && message.label.trim()
6666
+ ? message.label.trim()
6667
+ : "git diff";
6668
+ setEditorText(content, { preserveScroll: false, preserveSelection: false });
6669
+ setSourceState({ source: "blank", label, path: null });
6670
+ setEditorLanguage("diff");
6671
+ setBusy(false);
6672
+ setWsState("Ready");
6673
+ refreshResponseUi();
6674
+ setStatus(
6675
+ typeof message.message === "string" && message.message.trim()
6676
+ ? message.message
6677
+ : "Loaded current git diff.",
6678
+ "success",
6679
+ );
6680
+ return;
6681
+ }
6682
+
6365
6683
  if (message.type === "studio_state") {
6366
6684
  const busy = Boolean(message.busy);
6367
6685
  agentBusyFromServer = Boolean(message.agentBusy);
@@ -6458,8 +6776,17 @@ ${cssVarsBlock}
6458
6776
  }
6459
6777
 
6460
6778
  if (message.type === "info") {
6779
+ if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
6780
+ pendingRequestId = null;
6781
+ pendingKind = null;
6782
+ setBusy(false);
6783
+ setWsState("Ready");
6784
+ }
6461
6785
  if (typeof message.message === "string") {
6462
- setStatus(message.message);
6786
+ setStatus(
6787
+ message.message,
6788
+ typeof message.level === "string" ? message.level : undefined,
6789
+ );
6463
6790
  }
6464
6791
  }
6465
6792
 
@@ -7099,6 +7426,29 @@ ${cssVarsBlock}
7099
7426
  });
7100
7427
  }
7101
7428
 
7429
+ if (loadGitDiffBtn) {
7430
+ loadGitDiffBtn.addEventListener("click", () => {
7431
+ const requestId = beginUiAction("load_git_diff");
7432
+ if (!requestId) return;
7433
+
7434
+ const effectivePath = getEffectiveSavePath();
7435
+ const sent = sendMessage({
7436
+ type: "load_git_diff_request",
7437
+ requestId,
7438
+ sourcePath: effectivePath || sourceState.path || undefined,
7439
+ resourceDir: resourceDirInput && resourceDirInput.value.trim()
7440
+ ? resourceDirInput.value.trim()
7441
+ : undefined,
7442
+ });
7443
+
7444
+ if (!sent) {
7445
+ pendingRequestId = null;
7446
+ pendingKind = null;
7447
+ setBusy(false);
7448
+ }
7449
+ });
7450
+ }
7451
+
7102
7452
  sendRunBtn.addEventListener("click", () => {
7103
7453
  if (getAbortablePendingKind() === "direct") {
7104
7454
  requestCancelForPendingRequest("direct");
@@ -7709,6 +8059,43 @@ export default function (pi: ExtensionAPI) {
7709
8059
  return;
7710
8060
  }
7711
8061
 
8062
+ if (msg.type === "load_git_diff_request") {
8063
+ if (!isValidRequestId(msg.requestId)) {
8064
+ sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
8065
+ return;
8066
+ }
8067
+ if (isStudioBusy()) {
8068
+ sendToClient(client, { type: "busy", requestId: msg.requestId, message: "Studio is busy." });
8069
+ return;
8070
+ }
8071
+
8072
+ const baseDir = resolveStudioGitDiffBaseDir(msg.sourcePath, msg.resourceDir, studioCwd);
8073
+ const diffResult = readStudioGitDiff(baseDir);
8074
+ if (diffResult.ok === false) {
8075
+ sendToClient(client, {
8076
+ type: "info",
8077
+ requestId: msg.requestId,
8078
+ message: diffResult.message,
8079
+ level: diffResult.level,
8080
+ });
8081
+ return;
8082
+ }
8083
+
8084
+ initialStudioDocument = {
8085
+ text: diffResult.text,
8086
+ label: diffResult.label,
8087
+ source: "blank",
8088
+ };
8089
+ sendToClient(client, {
8090
+ type: "git_diff_snapshot",
8091
+ requestId: msg.requestId,
8092
+ content: diffResult.text,
8093
+ label: diffResult.label,
8094
+ message: "Loaded current git diff into Studio.",
8095
+ });
8096
+ return;
8097
+ }
8098
+
7712
8099
  if (msg.type === "cancel_request") {
7713
8100
  if (!isValidRequestId(msg.requestId)) {
7714
8101
  sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
@@ -7716,7 +8103,7 @@ export default function (pi: ExtensionAPI) {
7716
8103
  }
7717
8104
 
7718
8105
  const result = cancelActiveRequest(msg.requestId);
7719
- if (!result.ok) {
8106
+ if (result.ok === false) {
7720
8107
  sendToClient(client, { type: "error", requestId: msg.requestId, message: result.message });
7721
8108
  }
7722
8109
  return;
@@ -7914,7 +8301,7 @@ export default function (pi: ExtensionAPI) {
7914
8301
  }
7915
8302
 
7916
8303
  const result = writeStudioFile(msg.path, studioCwd, msg.content);
7917
- if (!result.ok) {
8304
+ if (result.ok === false) {
7918
8305
  sendToClient(client, { type: "error", requestId: msg.requestId, message: result.message });
7919
8306
  return;
7920
8307
  }
@@ -8778,7 +9165,7 @@ export default function (pi: ExtensionAPI) {
8778
9165
  }
8779
9166
 
8780
9167
  const file = readStudioFile(pathArg, ctx.cwd);
8781
- if (!file.ok) {
9168
+ if (file.ok === false) {
8782
9169
  ctx.ui.notify(file.message, "error");
8783
9170
  return;
8784
9171
  }
@@ -8844,7 +9231,7 @@ export default function (pi: ExtensionAPI) {
8844
9231
  }
8845
9232
 
8846
9233
  const file = readStudioFile(pathArg, ctx.cwd);
8847
- if (!file.ok) {
9234
+ if (file.ok === false) {
8848
9235
  ctx.ui.notify(file.message, "error");
8849
9236
  return;
8850
9237
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.11",
3
+ "version": "0.5.13",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",