pi-studio 0.5.10 → 0.5.12

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 +10 -0
  2. package/index.ts +410 -4
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ All notable changes to `pi-studio` are documented here.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.5.12] — 2026-03-15
8
+
9
+ ### Added
10
+ - 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`.
11
+
12
+ ## [0.5.11] — 2026-03-15
13
+
14
+ ### Added
15
+ - Studio tabs now show a title attention marker like `● Response ready` or `● Critique ready` when a Studio-started model request finishes while the tab is unfocused, and clear that marker when the tab regains focus or the next Studio request starts.
16
+
7
17
  ## [0.5.10] — 2026-03-14
8
18
 
9
19
  ### Fixed
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;
@@ -904,6 +912,207 @@ function writeStudioFile(pathArg: string, cwd: string, content: string):
904
912
  }
905
913
  }
906
914
 
915
+ function splitStudioGitPathOutput(output: string): string[] {
916
+ return output
917
+ .split(/\r?\n/)
918
+ .map((line) => line.trim())
919
+ .filter((line) => line.length > 0);
920
+ }
921
+
922
+ function formatStudioGitSpawnFailure(
923
+ result: { stdout?: string | Buffer | null; stderr?: string | Buffer | null },
924
+ args: string[],
925
+ ): string {
926
+ const stderr = typeof result.stderr === "string"
927
+ ? result.stderr.trim()
928
+ : (result.stderr ? result.stderr.toString("utf-8").trim() : "");
929
+ const stdout = typeof result.stdout === "string"
930
+ ? result.stdout.trim()
931
+ : (result.stdout ? result.stdout.toString("utf-8").trim() : "");
932
+ return stderr || stdout || `git ${args.join(" ")} failed`;
933
+ }
934
+
935
+ function readStudioTextFileIfPossible(path: string): string | null {
936
+ try {
937
+ const buf = readFileSync(path);
938
+ const sample = buf.subarray(0, 8192);
939
+ let nulCount = 0;
940
+ let controlCount = 0;
941
+ for (let i = 0; i < sample.length; i++) {
942
+ const b = sample[i];
943
+ if (b === 0x00) nulCount += 1;
944
+ else if (b < 0x08 || (b > 0x0D && b < 0x20 && b !== 0x1B)) controlCount += 1;
945
+ }
946
+ if (nulCount > 0 || (sample.length > 0 && controlCount / sample.length > 0.1)) {
947
+ return null;
948
+ }
949
+ return buf.toString("utf-8").replace(/\r\n/g, "\n");
950
+ } catch {
951
+ return null;
952
+ }
953
+ }
954
+
955
+ function buildStudioSyntheticNewFileDiff(filePath: string, content: string): string {
956
+ const lines = content.split("\n");
957
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
958
+ lines.pop();
959
+ }
960
+
961
+ const diffLines = [
962
+ `diff --git a/${filePath} b/${filePath}`,
963
+ "new file mode 100644",
964
+ "--- /dev/null",
965
+ `+++ b/${filePath}`,
966
+ `@@ -0,0 +1,${lines.length} @@`,
967
+ ];
968
+
969
+ if (lines.length > 0) {
970
+ diffLines.push(lines.map((line) => `+${line}`).join("\n"));
971
+ }
972
+
973
+ return diffLines.join("\n");
974
+ }
975
+
976
+ function resolveStudioGitDiffBaseDir(sourcePath: string | undefined, resourceDir: string | undefined, fallbackCwd: string): string {
977
+ const source = typeof sourcePath === "string" ? sourcePath.trim() : "";
978
+ if (source) {
979
+ return dirname(source);
980
+ }
981
+
982
+ const resource = typeof resourceDir === "string" ? resourceDir.trim() : "";
983
+ if (resource) {
984
+ return isAbsolute(resource) ? resource : resolve(fallbackCwd, resource);
985
+ }
986
+
987
+ return fallbackCwd;
988
+ }
989
+
990
+ function readStudioGitDiff(baseDir: string):
991
+ | { ok: true; text: string; label: string }
992
+ | { ok: false; level: "info" | "warning" | "error"; message: string } {
993
+ const repoRootArgs = ["rev-parse", "--show-toplevel"];
994
+ const repoRootResult = spawnSync("git", repoRootArgs, {
995
+ cwd: baseDir,
996
+ encoding: "utf-8",
997
+ });
998
+ if (repoRootResult.status !== 0) {
999
+ return {
1000
+ ok: false,
1001
+ level: "warning",
1002
+ message: "No git repository found for the current Studio context.",
1003
+ };
1004
+ }
1005
+ const repoRoot = repoRootResult.stdout.trim();
1006
+
1007
+ const hasHead = spawnSync("git", ["rev-parse", "--verify", "HEAD"], {
1008
+ cwd: repoRoot,
1009
+ encoding: "utf-8",
1010
+ }).status === 0;
1011
+
1012
+ const untrackedArgs = ["ls-files", "--others", "--exclude-standard"];
1013
+ const untrackedResult = spawnSync("git", untrackedArgs, {
1014
+ cwd: repoRoot,
1015
+ encoding: "utf-8",
1016
+ });
1017
+ if (untrackedResult.status !== 0) {
1018
+ return {
1019
+ ok: false,
1020
+ level: "error",
1021
+ message: `Failed to list untracked files: ${formatStudioGitSpawnFailure(untrackedResult, untrackedArgs)}`,
1022
+ };
1023
+ }
1024
+ const untrackedPaths = splitStudioGitPathOutput(untrackedResult.stdout ?? "").sort();
1025
+
1026
+ let diffOutput = "";
1027
+ let statSummary = "";
1028
+ let currentTreeFileCount = 0;
1029
+
1030
+ if (hasHead) {
1031
+ const diffArgs = ["diff", "HEAD", "--unified=3", "--find-renames", "--no-color", "--"];
1032
+ const diffResult = spawnSync("git", diffArgs, {
1033
+ cwd: repoRoot,
1034
+ encoding: "utf-8",
1035
+ });
1036
+ if (diffResult.status !== 0) {
1037
+ return {
1038
+ ok: false,
1039
+ level: "error",
1040
+ message: `Failed to collect git diff: ${formatStudioGitSpawnFailure(diffResult, diffArgs)}`,
1041
+ };
1042
+ }
1043
+ diffOutput = diffResult.stdout ?? "";
1044
+
1045
+ const statArgs = ["diff", "HEAD", "--stat", "--find-renames", "--no-color", "--"];
1046
+ const statResult = spawnSync("git", statArgs, {
1047
+ cwd: repoRoot,
1048
+ encoding: "utf-8",
1049
+ });
1050
+ if (statResult.status === 0) {
1051
+ const statLines = splitStudioGitPathOutput(statResult.stdout ?? "");
1052
+ statSummary = statLines.length > 0 ? (statLines[statLines.length - 1] ?? "") : "";
1053
+ }
1054
+ } else {
1055
+ const trackedArgs = ["ls-files", "--cached"];
1056
+ const trackedResult = spawnSync("git", trackedArgs, {
1057
+ cwd: repoRoot,
1058
+ encoding: "utf-8",
1059
+ });
1060
+ if (trackedResult.status !== 0) {
1061
+ return {
1062
+ ok: false,
1063
+ level: "error",
1064
+ message: `Failed to inspect tracked files: ${formatStudioGitSpawnFailure(trackedResult, trackedArgs)}`,
1065
+ };
1066
+ }
1067
+
1068
+ const trackedPaths = splitStudioGitPathOutput(trackedResult.stdout ?? "");
1069
+ const currentTreePaths = Array.from(new Set([...trackedPaths, ...untrackedPaths])).sort();
1070
+ currentTreeFileCount = currentTreePaths.length;
1071
+ diffOutput = currentTreePaths
1072
+ .map((filePath) => {
1073
+ const content = readStudioTextFileIfPossible(join(repoRoot, filePath));
1074
+ if (content == null) return "";
1075
+ return buildStudioSyntheticNewFileDiff(filePath, content);
1076
+ })
1077
+ .filter((section) => section.length > 0)
1078
+ .join("\n\n");
1079
+ }
1080
+
1081
+ const untrackedSections = hasHead
1082
+ ? untrackedPaths
1083
+ .map((filePath) => {
1084
+ const content = readStudioTextFileIfPossible(join(repoRoot, filePath));
1085
+ if (content == null) return "";
1086
+ return buildStudioSyntheticNewFileDiff(filePath, content);
1087
+ })
1088
+ .filter((section) => section.length > 0)
1089
+ : [];
1090
+
1091
+ const fullDiff = [diffOutput.trimEnd(), ...untrackedSections].filter(Boolean).join("\n\n");
1092
+ if (!fullDiff.trim()) {
1093
+ return {
1094
+ ok: false,
1095
+ level: "info",
1096
+ message: "No uncommitted git changes to load.",
1097
+ };
1098
+ }
1099
+
1100
+ const summaryParts: string[] = [];
1101
+ if (hasHead && statSummary) {
1102
+ summaryParts.push(statSummary);
1103
+ }
1104
+ if (!hasHead && currentTreeFileCount > 0) {
1105
+ summaryParts.push(`${currentTreeFileCount} file${currentTreeFileCount === 1 ? "" : "s"} in current tree`);
1106
+ }
1107
+ if (untrackedPaths.length > 0) {
1108
+ summaryParts.push(`${untrackedPaths.length} untracked file${untrackedPaths.length === 1 ? "" : "s"}`);
1109
+ }
1110
+
1111
+ const labelBase = hasHead ? "git diff HEAD" : "git diff (no commits yet)";
1112
+ const label = summaryParts.length > 0 ? `${labelBase} (${summaryParts.join(", ")})` : labelBase;
1113
+ return { ok: true, text: fullDiff, label };
1114
+ }
1115
+
907
1116
  function readLocalPackageMetadata(): { name: string; version: string } | null {
908
1117
  try {
909
1118
  const raw = readFileSync(new URL("./package.json", import.meta.url), "utf-8");
@@ -1904,6 +2113,20 @@ function parseIncomingMessage(data: RawData): IncomingStudioMessage | null {
1904
2113
  };
1905
2114
  }
1906
2115
 
2116
+ if (
2117
+ msg.type === "load_git_diff_request"
2118
+ && typeof msg.requestId === "string"
2119
+ && (msg.sourcePath === undefined || typeof msg.sourcePath === "string")
2120
+ && (msg.resourceDir === undefined || typeof msg.resourceDir === "string")
2121
+ ) {
2122
+ return {
2123
+ type: "load_git_diff_request",
2124
+ requestId: msg.requestId,
2125
+ sourcePath: typeof msg.sourcePath === "string" ? msg.sourcePath : undefined,
2126
+ resourceDir: typeof msg.resourceDir === "string" ? msg.resourceDir : undefined,
2127
+ };
2128
+ }
2129
+
1907
2130
  if (msg.type === "cancel_request" && typeof msg.requestId === "string") {
1908
2131
  return {
1909
2132
  type: "cancel_request",
@@ -3258,6 +3481,8 @@ ${cssVarsBlock}
3258
3481
  <button id="saveAsBtn" type="button" title="Save editor content to a new file path.">Save editor as…</button>
3259
3482
  <button id="saveOverBtn" type="button" title="Overwrite current file with editor content." disabled>Save editor</button>
3260
3483
  <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>
3484
+ <button id="loadGitDiffBtn" type="button" title="Load the current git diff from the Studio context into the editor.">Load git diff</button>
3485
+ <button id="getEditorBtn" type="button" title="Load the current terminal editor draft into Studio.">Load from pi editor</button>
3261
3486
  </div>
3262
3487
  </header>
3263
3488
 
@@ -3286,7 +3511,6 @@ ${cssVarsBlock}
3286
3511
  <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
3512
  <button id="copyDraftBtn" type="button">Copy editor text</button>
3288
3513
  <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
3514
  </div>
3291
3515
  <div class="source-actions-row">
3292
3516
  <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 +3707,7 @@ ${cssVarsBlock}
3483
3707
  const saveOverBtn = document.getElementById("saveOverBtn");
3484
3708
  const sendEditorBtn = document.getElementById("sendEditorBtn");
3485
3709
  const getEditorBtn = document.getElementById("getEditorBtn");
3710
+ const loadGitDiffBtn = document.getElementById("loadGitDiffBtn");
3486
3711
  const sendRunBtn = document.getElementById("sendRunBtn");
3487
3712
  const copyDraftBtn = document.getElementById("copyDraftBtn");
3488
3713
  const saveAnnotatedBtn = document.getElementById("saveAnnotatedBtn");
@@ -3539,6 +3764,10 @@ ${cssVarsBlock}
3539
3764
  let contextPercent = null;
3540
3765
  let updateInstalledVersion = null;
3541
3766
  let updateLatestVersion = null;
3767
+ let windowHasFocus = typeof document.hasFocus === "function" ? document.hasFocus() : true;
3768
+ let titleAttentionMessage = "";
3769
+ let titleAttentionRequestId = null;
3770
+ let titleAttentionRequestKind = null;
3542
3771
 
3543
3772
  function parseFiniteNumber(value) {
3544
3773
  if (value == null || value === "") return null;
@@ -3769,6 +3998,7 @@ ${cssVarsBlock}
3769
3998
  if (kind === "compact") return "compacting context";
3770
3999
  if (kind === "send_to_editor") return "sending to pi editor";
3771
4000
  if (kind === "get_from_editor") return "loading from pi editor";
4001
+ if (kind === "load_git_diff") return "loading git diff";
3772
4002
  if (kind === "save_as" || kind === "save_over") return "saving editor text";
3773
4003
  return "submitting request";
3774
4004
  }
@@ -3899,12 +4129,63 @@ ${cssVarsBlock}
3899
4129
  return changed;
3900
4130
  }
3901
4131
 
4132
+ function isTitleAttentionRequestKind(kind) {
4133
+ return kind === "annotation" || kind === "critique" || kind === "direct";
4134
+ }
4135
+
4136
+ function armTitleAttentionForRequest(requestId, kind) {
4137
+ if (typeof requestId !== "string" || !isTitleAttentionRequestKind(kind)) {
4138
+ titleAttentionRequestId = null;
4139
+ titleAttentionRequestKind = null;
4140
+ return;
4141
+ }
4142
+ titleAttentionRequestId = requestId;
4143
+ titleAttentionRequestKind = kind;
4144
+ }
4145
+
4146
+ function clearArmedTitleAttention(requestId) {
4147
+ if (typeof requestId === "string" && titleAttentionRequestId && requestId !== titleAttentionRequestId) {
4148
+ return;
4149
+ }
4150
+ titleAttentionRequestId = null;
4151
+ titleAttentionRequestKind = null;
4152
+ }
4153
+
4154
+ function clearTitleAttention() {
4155
+ if (!titleAttentionMessage) return;
4156
+ titleAttentionMessage = "";
4157
+ updateDocumentTitle();
4158
+ }
4159
+
4160
+ function shouldShowTitleAttention() {
4161
+ const focused = typeof document.hasFocus === "function" ? document.hasFocus() : windowHasFocus;
4162
+ return Boolean(document.hidden) || !focused;
4163
+ }
4164
+
4165
+ function getTitleAttentionMessage(kind) {
4166
+ if (kind === "critique") return "● Critique ready";
4167
+ if (kind === "direct") return "● Response ready";
4168
+ return "● Reply ready";
4169
+ }
4170
+
4171
+ function maybeShowTitleAttentionForCompletedRequest(requestId, kind) {
4172
+ const matchedRequest = typeof requestId === "string" && titleAttentionRequestId && requestId === titleAttentionRequestId;
4173
+ const completedKind = isTitleAttentionRequestKind(kind) ? kind : titleAttentionRequestKind;
4174
+ clearArmedTitleAttention(requestId);
4175
+ if (!matchedRequest || !completedKind || !shouldShowTitleAttention()) {
4176
+ return;
4177
+ }
4178
+ titleAttentionMessage = getTitleAttentionMessage(completedKind);
4179
+ updateDocumentTitle();
4180
+ }
4181
+
3902
4182
  function updateDocumentTitle() {
3903
4183
  const modelText = modelLabel && modelLabel.trim() ? modelLabel.trim() : "none";
3904
4184
  const terminalText = terminalSessionLabel && terminalSessionLabel.trim() ? terminalSessionLabel.trim() : "unknown";
3905
4185
  const titleParts = ["pi Studio"];
3906
4186
  if (terminalText && terminalText !== "unknown") titleParts.push(terminalText);
3907
4187
  if (modelText && modelText !== "none") titleParts.push(modelText);
4188
+ if (titleAttentionMessage) titleParts.unshift(titleAttentionMessage);
3908
4189
  document.title = titleParts.join(" · ");
3909
4190
  }
3910
4191
 
@@ -3998,6 +4279,24 @@ ${cssVarsBlock}
3998
4279
 
3999
4280
  renderStatus();
4000
4281
 
4282
+ window.addEventListener("focus", () => {
4283
+ windowHasFocus = true;
4284
+ clearTitleAttention();
4285
+ });
4286
+
4287
+ window.addEventListener("blur", () => {
4288
+ windowHasFocus = false;
4289
+ });
4290
+
4291
+ document.addEventListener("visibilitychange", () => {
4292
+ if (!document.hidden) {
4293
+ windowHasFocus = typeof document.hasFocus === "function" ? document.hasFocus() : windowHasFocus;
4294
+ if (windowHasFocus) {
4295
+ clearTitleAttention();
4296
+ }
4297
+ }
4298
+ });
4299
+
4001
4300
  function updateSourceBadge() {
4002
4301
  const label = sourceState && sourceState.label ? sourceState.label : "blank";
4003
4302
  sourceBadgeEl.textContent = "Editor origin: " + label;
@@ -5090,6 +5389,7 @@ ${cssVarsBlock}
5090
5389
  saveOverBtn.disabled = uiBusy || !canSaveOver;
5091
5390
  sendEditorBtn.disabled = uiBusy;
5092
5391
  if (getEditorBtn) getEditorBtn.disabled = uiBusy;
5392
+ if (loadGitDiffBtn) loadGitDiffBtn.disabled = uiBusy;
5093
5393
  syncRunAndCritiqueButtons();
5094
5394
  copyDraftBtn.disabled = uiBusy;
5095
5395
  if (highlightSelect) highlightSelect.disabled = uiBusy;
@@ -5810,8 +6110,10 @@ ${cssVarsBlock}
5810
6110
  setStatus("No matching Studio request is running.", "warning");
5811
6111
  return false;
5812
6112
  }
5813
- const sent = sendMessage({ type: "cancel_request", requestId: pendingRequestId });
6113
+ const requestId = pendingRequestId;
6114
+ const sent = sendMessage({ type: "cancel_request", requestId });
5814
6115
  if (!sent) return false;
6116
+ clearArmedTitleAttention(requestId);
5815
6117
  setStatus("Stopping request…", "warning");
5816
6118
  return true;
5817
6119
  }
@@ -6119,6 +6421,7 @@ ${cssVarsBlock}
6119
6421
  return;
6120
6422
  }
6121
6423
 
6424
+ const completedRequestId = typeof message.requestId === "string" ? message.requestId : pendingRequestId;
6122
6425
  const responseKind =
6123
6426
  typeof message.kind === "string"
6124
6427
  ? message.kind
@@ -6151,6 +6454,7 @@ ${cssVarsBlock}
6151
6454
  } else {
6152
6455
  setStatus("Response ready.", "success");
6153
6456
  }
6457
+ maybeShowTitleAttentionForCompletedRequest(completedRequestId, responseKind);
6154
6458
  return;
6155
6459
  }
6156
6460
 
@@ -6285,6 +6589,31 @@ ${cssVarsBlock}
6285
6589
  return;
6286
6590
  }
6287
6591
 
6592
+ if (message.type === "git_diff_snapshot") {
6593
+ if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
6594
+ pendingRequestId = null;
6595
+ pendingKind = null;
6596
+ }
6597
+
6598
+ const content = typeof message.content === "string" ? message.content : "";
6599
+ const label = typeof message.label === "string" && message.label.trim()
6600
+ ? message.label.trim()
6601
+ : "git diff";
6602
+ setEditorText(content, { preserveScroll: false, preserveSelection: false });
6603
+ setSourceState({ source: "blank", label, path: null });
6604
+ setEditorLanguage("diff");
6605
+ setBusy(false);
6606
+ setWsState("Ready");
6607
+ refreshResponseUi();
6608
+ setStatus(
6609
+ typeof message.message === "string" && message.message.trim()
6610
+ ? message.message
6611
+ : "Loaded current git diff.",
6612
+ "success",
6613
+ );
6614
+ return;
6615
+ }
6616
+
6288
6617
  if (message.type === "studio_state") {
6289
6618
  const busy = Boolean(message.busy);
6290
6619
  agentBusyFromServer = Boolean(message.agentBusy);
@@ -6352,6 +6681,9 @@ ${cssVarsBlock}
6352
6681
  pendingRequestId = null;
6353
6682
  pendingKind = null;
6354
6683
  }
6684
+ if (typeof message.requestId === "string") {
6685
+ clearArmedTitleAttention(message.requestId);
6686
+ }
6355
6687
  stickyStudioKind = null;
6356
6688
  setBusy(false);
6357
6689
  setWsState("Ready");
@@ -6367,6 +6699,9 @@ ${cssVarsBlock}
6367
6699
  pendingRequestId = null;
6368
6700
  pendingKind = null;
6369
6701
  }
6702
+ if (typeof message.requestId === "string") {
6703
+ clearArmedTitleAttention(message.requestId);
6704
+ }
6370
6705
  stickyStudioKind = null;
6371
6706
  setBusy(false);
6372
6707
  setWsState("Ready");
@@ -6375,8 +6710,17 @@ ${cssVarsBlock}
6375
6710
  }
6376
6711
 
6377
6712
  if (message.type === "info") {
6713
+ if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
6714
+ pendingRequestId = null;
6715
+ pendingKind = null;
6716
+ setBusy(false);
6717
+ setWsState("Ready");
6718
+ }
6378
6719
  if (typeof message.message === "string") {
6379
- setStatus(message.message);
6720
+ setStatus(
6721
+ message.message,
6722
+ typeof message.level === "string" ? message.level : undefined,
6723
+ );
6380
6724
  }
6381
6725
  }
6382
6726
 
@@ -6525,10 +6869,12 @@ ${cssVarsBlock}
6525
6869
  setStatus("Studio is busy.", "warning");
6526
6870
  return null;
6527
6871
  }
6872
+ clearTitleAttention();
6528
6873
  const requestId = makeRequestId();
6529
6874
  pendingRequestId = requestId;
6530
6875
  pendingKind = kind;
6531
6876
  stickyStudioKind = kind;
6877
+ armTitleAttentionForRequest(requestId, kind);
6532
6878
  setBusy(true);
6533
6879
  setWsState("Submitting");
6534
6880
  setStatus(getStudioBusyStatus(kind), "warning");
@@ -7014,6 +7360,29 @@ ${cssVarsBlock}
7014
7360
  });
7015
7361
  }
7016
7362
 
7363
+ if (loadGitDiffBtn) {
7364
+ loadGitDiffBtn.addEventListener("click", () => {
7365
+ const requestId = beginUiAction("load_git_diff");
7366
+ if (!requestId) return;
7367
+
7368
+ const effectivePath = getEffectiveSavePath();
7369
+ const sent = sendMessage({
7370
+ type: "load_git_diff_request",
7371
+ requestId,
7372
+ sourcePath: effectivePath || sourceState.path || undefined,
7373
+ resourceDir: resourceDirInput && resourceDirInput.value.trim()
7374
+ ? resourceDirInput.value.trim()
7375
+ : undefined,
7376
+ });
7377
+
7378
+ if (!sent) {
7379
+ pendingRequestId = null;
7380
+ pendingKind = null;
7381
+ setBusy(false);
7382
+ }
7383
+ });
7384
+ }
7385
+
7017
7386
  sendRunBtn.addEventListener("click", () => {
7018
7387
  if (getAbortablePendingKind() === "direct") {
7019
7388
  requestCancelForPendingRequest("direct");
@@ -7624,6 +7993,43 @@ export default function (pi: ExtensionAPI) {
7624
7993
  return;
7625
7994
  }
7626
7995
 
7996
+ if (msg.type === "load_git_diff_request") {
7997
+ if (!isValidRequestId(msg.requestId)) {
7998
+ sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
7999
+ return;
8000
+ }
8001
+ if (isStudioBusy()) {
8002
+ sendToClient(client, { type: "busy", requestId: msg.requestId, message: "Studio is busy." });
8003
+ return;
8004
+ }
8005
+
8006
+ const baseDir = resolveStudioGitDiffBaseDir(msg.sourcePath, msg.resourceDir, studioCwd);
8007
+ const diffResult = readStudioGitDiff(baseDir);
8008
+ if (!diffResult.ok) {
8009
+ sendToClient(client, {
8010
+ type: "info",
8011
+ requestId: msg.requestId,
8012
+ message: diffResult.message,
8013
+ level: diffResult.level,
8014
+ });
8015
+ return;
8016
+ }
8017
+
8018
+ initialStudioDocument = {
8019
+ text: diffResult.text,
8020
+ label: diffResult.label,
8021
+ source: "blank",
8022
+ };
8023
+ sendToClient(client, {
8024
+ type: "git_diff_snapshot",
8025
+ requestId: msg.requestId,
8026
+ content: diffResult.text,
8027
+ label: diffResult.label,
8028
+ message: "Loaded current git diff into Studio.",
8029
+ });
8030
+ return;
8031
+ }
8032
+
7627
8033
  if (msg.type === "cancel_request") {
7628
8034
  if (!isValidRequestId(msg.requestId)) {
7629
8035
  sendToClient(client, { type: "error", requestId: msg.requestId, message: "Invalid request ID." });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-studio",
3
- "version": "0.5.10",
3
+ "version": "0.5.12",
4
4
  "description": "Browser GUI for structured critique workflows in pi",
5
5
  "type": "module",
6
6
  "license": "MIT",