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.
- package/CHANGELOG.md +12 -0
- package/index.ts +412 -25
- 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 (
|
|
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 (
|
|
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" ?
|
|
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
|
|
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 +=
|
|
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
|
|
5558
|
-
if (/^@@/.test(source)) return "<span class=\\"hl-code-fn\\">" +
|
|
5559
|
-
if (/^\\+\\+\\+|^---/.test(source)) return "<span class=\\"hl-code-kw\\">" +
|
|
5560
|
-
if (/^\\+/.test(source)) return "<span class=\\"hl-diff-add\\">" +
|
|
5561
|
-
if (/^-/.test(source)) return "<span class=\\"hl-diff-del\\">" +
|
|
5562
|
-
if (/^diff /.test(source)) return "<span class=\\"hl-code-kw\\">" +
|
|
5563
|
-
if (/^index /.test(source)) return "<span class=\\"hl-code-com\\">" +
|
|
5564
|
-
return
|
|
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(
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
9234
|
+
if (file.ok === false) {
|
|
8848
9235
|
ctx.ui.notify(file.message, "error");
|
|
8849
9236
|
return;
|
|
8850
9237
|
}
|