pi-studio 0.9.23 → 0.9.25

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.
@@ -110,7 +110,6 @@
110
110
  const openCompanionBtn = document.getElementById("openCompanionBtn");
111
111
  const getEditorBtn = document.getElementById("getEditorBtn");
112
112
  const zenModeBtn = document.getElementById("zenModeBtn");
113
- const loadGitDiffBtn = document.getElementById("loadGitDiffBtn");
114
113
  const sendRunBtn = document.getElementById("sendRunBtn");
115
114
  const queueSteerBtn = document.getElementById("queueSteerBtn");
116
115
  const sendReplBtn = document.getElementById("sendReplBtn");
@@ -237,6 +236,7 @@
237
236
  let sourceResetOriginBtn = null;
238
237
  let sourceOpenCurrentFileTabBtn = null;
239
238
  let sourceOpenCurrentTextCopyTabBtn = null;
239
+ let sourceSessionSummaryEl = null;
240
240
  let initialDocumentApplied = false;
241
241
  function normalizeRightViewValue(nextView) {
242
242
  const raw = String(nextView || "").trim();
@@ -248,8 +248,10 @@
248
248
  ? "repl"
249
249
  : (raw === "files"
250
250
  ? "files"
251
- : ((raw === "trace" || raw === "thinking") ? "trace" : "markdown"))));
252
- if (isEditorOnlyMode && normalized !== "editor-preview" && normalized !== "files" && normalized !== "repl") {
251
+ : (raw === "changes"
252
+ ? "changes"
253
+ : ((raw === "trace" || raw === "thinking") ? "trace" : "markdown")))));
254
+ if (isEditorOnlyMode && normalized !== "editor-preview" && normalized !== "files" && normalized !== "changes" && normalized !== "repl") {
253
255
  return "editor-preview";
254
256
  }
255
257
  return normalized;
@@ -257,13 +259,13 @@
257
259
 
258
260
  function syncRightViewModeOptions() {
259
261
  if (!rightViewSelect || !rightViewSelect.options) return;
260
- const editorOnlyAllowed = new Set(["editor-preview", "files", "repl"]);
262
+ const editorOnlyAllowed = new Set(["editor-preview", "files", "changes", "repl"]);
261
263
  Array.from(rightViewSelect.options).forEach((option) => {
262
264
  if (!option) return;
263
265
  option.disabled = isEditorOnlyMode && !editorOnlyAllowed.has(option.value);
264
266
  });
265
267
  rightViewSelect.title = isEditorOnlyMode
266
- ? "Editor-only views: editor preview, Files, or REPL. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+P switches directly to Preview."
268
+ ? "Editor-only views: editor preview, Changes, Files, or REPL. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+P switches directly to Preview."
267
269
  : "Right pane view mode. F7 cycles when the right pane is active; Cmd/Ctrl+Alt+P switches directly to Preview.";
268
270
  }
269
271
 
@@ -296,6 +298,19 @@
296
298
  let traceAutoScroll = true;
297
299
  let traceRenderRaf = null;
298
300
  const traceExpandedOutputs = new Set();
301
+ let gitChangesState = {
302
+ status: "idle",
303
+ requestId: null,
304
+ content: "",
305
+ label: "",
306
+ repoRoot: "",
307
+ branch: "",
308
+ hasHead: true,
309
+ files: [],
310
+ selectedPath: "",
311
+ message: "",
312
+ level: "info",
313
+ };
299
314
  const TRACE_OUTPUT_PREVIEW_MAX_LINES = 50;
300
315
  const TRACE_OUTPUT_PREVIEW_MAX_CHARS = 8000;
301
316
  const TRACE_IMAGE_SAFE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/gif", "image/webp"]);
@@ -2485,9 +2500,19 @@
2485
2500
  });
2486
2501
  });
2487
2502
  appendStudioUiRefreshMenuSection(contextMenu.menu, "Suggestions", [cursorContextBtn, sessionContextBtn]);
2503
+ const statusItems = [];
2504
+ if (!isEditorOnlyMode) {
2505
+ sourceSessionSummaryEl = makeStudioUiRefreshElement("div", "source-badge source-session-summary", "Session tree: branch history follows the current Pi branch. Editor text is independent.");
2506
+ sourceSessionSummaryEl.setAttribute("aria-label", "Pi session tree and editor sync behaviour");
2507
+ sourceSessionSummaryEl.title = "Use /tree in the Pi terminal to navigate branches. Studio updates branch history to match the active branch and leaves editor text unchanged.";
2508
+ statusItems.push(sourceSessionSummaryEl);
2509
+ }
2488
2510
  if (syncBadgeEl) {
2489
2511
  syncBadgeEl.hidden = false;
2490
- appendStudioUiRefreshMenuSection(contextMenu.menu, "Status", [syncBadgeEl]);
2512
+ statusItems.push(syncBadgeEl);
2513
+ }
2514
+ if (statusItems.length > 0) {
2515
+ appendStudioUiRefreshMenuSection(contextMenu.menu, "Status", statusItems);
2491
2516
  }
2492
2517
  }
2493
2518
 
@@ -3720,12 +3745,12 @@
3720
3745
 
3721
3746
  function triggerResponseHistoryShortcut(action) {
3722
3747
  if (isEditorOnlyMode) {
3723
- setStatus("Response history is unavailable in editor-only Studio.", "warning");
3748
+ setStatus("Branch history is unavailable in editor-only Studio.", "warning");
3724
3749
  return false;
3725
3750
  }
3726
3751
  const total = Array.isArray(responseHistory) ? responseHistory.length : 0;
3727
3752
  if (total <= 0) {
3728
- setStatus("No response history available yet.", "warning");
3753
+ setStatus("No branch history available yet.", "warning");
3729
3754
  return false;
3730
3755
  }
3731
3756
  if (action === "previous") {
@@ -4208,7 +4233,7 @@
4208
4233
  ? responseHistoryIndex + 1
4209
4234
  : 0;
4210
4235
  if (historyIndexBadgeEl) {
4211
- historyIndexBadgeEl.textContent = "History: " + selected + "/" + total;
4236
+ historyIndexBadgeEl.textContent = "Branch history: " + selected + "/" + total;
4212
4237
  }
4213
4238
  if (historyPrevBtn) {
4214
4239
  historyPrevBtn.disabled = total <= 1 || responseHistoryIndex <= 0;
@@ -4267,7 +4292,7 @@
4267
4292
  const item = getSelectedHistoryItem();
4268
4293
  if (item) {
4269
4294
  const responseLabel = item.kind === "critique" ? "critique" : "response";
4270
- setStatus("Viewing " + responseLabel + " history " + (nextIndex + 1) + "/" + total + ".");
4295
+ setStatus("Viewing " + responseLabel + " in current branch history " + (nextIndex + 1) + "/" + total + ".");
4271
4296
  }
4272
4297
  }
4273
4298
  return applied;
@@ -4337,6 +4362,14 @@
4337
4362
  return;
4338
4363
  }
4339
4364
 
4365
+ if (rightView === "changes") {
4366
+ const count = getGitChangedFiles().length;
4367
+ referenceBadgeEl.textContent = gitChangesState.status === "loading"
4368
+ ? "Changes: loading"
4369
+ : (count ? ("Changes: " + count + " file" + (count === 1 ? "" : "s")) : "Changes: none");
4370
+ return;
4371
+ }
4372
+
4340
4373
  if (rightView === "trace") {
4341
4374
  const state = traceState || createEmptyTraceState();
4342
4375
  const context = traceDisplayContext || {};
@@ -4389,7 +4422,7 @@
4389
4422
  const selected = total > 0 && responseHistoryIndex >= 0 && responseHistoryIndex < total
4390
4423
  ? responseHistoryIndex + 1
4391
4424
  : 0;
4392
- const historyPrefix = total > 0 ? "Response history " + selected + "/" + total + " · " : "";
4425
+ const historyPrefix = total > 0 ? "Branch history " + selected + "/" + total + " · " : "";
4393
4426
  referenceBadgeEl.textContent = time
4394
4427
  ? historyPrefix + responseLabel + " · " + time
4395
4428
  : historyPrefix + responseLabel;
@@ -7788,6 +7821,7 @@
7788
7821
  critiqueViewEl.addEventListener("click", handleTracePaneClick);
7789
7822
  critiqueViewEl.addEventListener("click", handleReplPaneClick);
7790
7823
  critiqueViewEl.addEventListener("click", handleFilesPaneClick);
7824
+ critiqueViewEl.addEventListener("click", handleGitChangesPaneClick);
7791
7825
  critiqueViewEl.addEventListener("change", handleReplPaneChange);
7792
7826
  }
7793
7827
 
@@ -8984,6 +9018,166 @@
8984
9018
  + "</div>";
8985
9019
  }
8986
9020
 
9021
+ function parseTraceToolArgsObject(inputText) {
9022
+ const value = String(inputText || "").trim();
9023
+ if (!value || (value[0] !== "{" && value[0] !== "[")) return null;
9024
+ try {
9025
+ const parsed = JSON.parse(value);
9026
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
9027
+ } catch {
9028
+ return null;
9029
+ }
9030
+ }
9031
+
9032
+ function countTraceTextLines(text) {
9033
+ const value = String(text || "");
9034
+ return value ? value.split(/\n/).length : 0;
9035
+ }
9036
+
9037
+ function formatTraceTextMetrics(text) {
9038
+ const value = String(text || "");
9039
+ const lines = countTraceTextLines(value);
9040
+ const chars = value.length;
9041
+ return formatCompactNumber(lines) + " line" + (lines === 1 ? "" : "s")
9042
+ + ", " + formatCompactNumber(chars) + " char" + (chars === 1 ? "" : "s");
9043
+ }
9044
+
9045
+ function renderTraceToolField(label, value, className) {
9046
+ const text = String(value || "").trim();
9047
+ if (!text) return "";
9048
+ const extraClass = className ? " " + String(className) : "";
9049
+ return "<div class='trace-tool-field'>"
9050
+ + "<span class='trace-tool-field-label'>" + escapeHtml(label) + "</span>"
9051
+ + "<code class='trace-tool-field-value" + extraClass + "' title='" + escapeHtml(text) + "'>" + escapeHtml(text) + "</code>"
9052
+ + "</div>";
9053
+ }
9054
+
9055
+ function renderTraceRawInputDetails(inputText, outputKey) {
9056
+ const value = String(inputText || "").trim();
9057
+ if (!value) return "";
9058
+ const rawKey = outputKey + ":raw-input";
9059
+ const openAttr = traceExpandedOutputs.has(rawKey) ? " open" : "";
9060
+ return "<details class='trace-tool-details trace-tool-raw-input'" + openAttr + ">"
9061
+ + "<summary>Raw input</summary>"
9062
+ + "<div class='trace-tool-details-body'>"
9063
+ + renderTraceOutput(value, rawKey, { label: "Raw input" })
9064
+ + "</div>"
9065
+ + "</details>";
9066
+ }
9067
+
9068
+ function renderTraceToolTextDetails(summary, text, outputKey, label, options) {
9069
+ const value = String(text || "");
9070
+ const emptyText = options && typeof options.emptyText === "string" ? options.emptyText : "[empty]";
9071
+ const openAttr = traceExpandedOutputs.has(outputKey) ? " open" : "";
9072
+ return "<details class='trace-tool-details" + (options && options.className ? " " + escapeHtml(options.className) : "") + "'" + openAttr + ">"
9073
+ + "<summary>" + escapeHtml(summary) + "</summary>"
9074
+ + "<div class='trace-tool-details-body'>"
9075
+ + renderTraceOutput(value || emptyText, outputKey, { label })
9076
+ + "</div>"
9077
+ + "</details>";
9078
+ }
9079
+
9080
+ function renderTraceEditInput(entry, payload, inputText) {
9081
+ const path = payload && typeof payload.path === "string" ? payload.path : "";
9082
+ const edits = payload && Array.isArray(payload.edits) ? payload.edits : [];
9083
+ const replacements = edits
9084
+ .map((edit, index) => {
9085
+ const item = edit && typeof edit === "object" ? edit : {};
9086
+ return {
9087
+ index,
9088
+ oldText: typeof item.oldText === "string" ? item.oldText : "",
9089
+ newText: typeof item.newText === "string" ? item.newText : "",
9090
+ };
9091
+ })
9092
+ .filter((edit) => edit.oldText || edit.newText);
9093
+ const replacementCount = replacements.length || edits.length;
9094
+ const fields = "<div class='trace-tool-fields'>"
9095
+ + renderTraceToolField("Path", path, "trace-tool-path")
9096
+ + renderTraceToolField("Changes", replacementCount + " replacement" + (replacementCount === 1 ? "" : "s"), "")
9097
+ + "</div>";
9098
+ const changes = replacements.length
9099
+ ? "<div class='trace-tool-change-list'>" + replacements.map((edit, displayIndex) => {
9100
+ const oldMetrics = formatTraceTextMetrics(edit.oldText);
9101
+ const newMetrics = formatTraceTextMetrics(edit.newText);
9102
+ const oldKey = entry.id + ":edit:" + edit.index + ":old";
9103
+ const newKey = entry.id + ":edit:" + edit.index + ":new";
9104
+ const openAttr = traceExpandedOutputs.has(oldKey) || traceExpandedOutputs.has(newKey) ? " open" : "";
9105
+ return "<details class='trace-tool-details trace-tool-change'" + openAttr + ">"
9106
+ + "<summary>Replacement " + escapeHtml(String(displayIndex + 1)) + " · " + escapeHtml(oldMetrics) + " → " + escapeHtml(newMetrics) + "</summary>"
9107
+ + "<div class='trace-tool-change-body'>"
9108
+ + "<div class='trace-tool-change-grid'>"
9109
+ + "<div class='trace-tool-change-column'><div class='trace-tool-code-label'>Old text</div>"
9110
+ + renderTraceOutput(edit.oldText || "[empty]", oldKey, { label: "Old text" })
9111
+ + "</div>"
9112
+ + "<div class='trace-tool-change-column'><div class='trace-tool-code-label'>New text</div>"
9113
+ + renderTraceOutput(edit.newText || "[empty]", newKey, { label: "New text" })
9114
+ + "</div>"
9115
+ + "</div>"
9116
+ + "</div>"
9117
+ + "</details>";
9118
+ }).join("") + "</div>"
9119
+ : "";
9120
+ return "<div class='trace-tool-input trace-tool-input-edit'>"
9121
+ + fields
9122
+ + changes
9123
+ + renderTraceRawInputDetails(inputText, entry.id + ":input")
9124
+ + "</div>";
9125
+ }
9126
+
9127
+ function renderTraceWriteInput(entry, payload, inputText) {
9128
+ const path = payload && typeof payload.path === "string" ? payload.path : "";
9129
+ const content = payload && typeof payload.content === "string" ? payload.content : null;
9130
+ const fields = "<div class='trace-tool-fields'>"
9131
+ + renderTraceToolField("Path", path, "trace-tool-path")
9132
+ + (content !== null ? renderTraceToolField("Content", formatTraceTextMetrics(content), "") : "")
9133
+ + "</div>";
9134
+ const contentDetails = content !== null
9135
+ ? renderTraceToolTextDetails("Content · " + formatTraceTextMetrics(content), content, entry.id + ":write:content", "Content", { className: "trace-tool-content" })
9136
+ : "";
9137
+ return "<div class='trace-tool-input trace-tool-input-write'>"
9138
+ + fields
9139
+ + contentDetails
9140
+ + renderTraceRawInputDetails(inputText, entry.id + ":input")
9141
+ + "</div>";
9142
+ }
9143
+
9144
+ function renderTraceReadInput(entry, payload, inputText) {
9145
+ const path = payload && typeof payload.path === "string" ? payload.path : "";
9146
+ const offset = payload && (typeof payload.offset === "number" || typeof payload.offset === "string") ? String(payload.offset) : "";
9147
+ const limit = payload && (typeof payload.limit === "number" || typeof payload.limit === "string") ? String(payload.limit) : "";
9148
+ const fields = "<div class='trace-tool-fields'>"
9149
+ + renderTraceToolField("Path", path, "trace-tool-path")
9150
+ + renderTraceToolField("Offset", offset ? "line " + offset : "", "")
9151
+ + renderTraceToolField("Limit", limit ? limit + " lines" : "", "")
9152
+ + "</div>";
9153
+ return "<div class='trace-tool-input trace-tool-input-read'>"
9154
+ + fields
9155
+ + renderTraceRawInputDetails(inputText, entry.id + ":input")
9156
+ + "</div>";
9157
+ }
9158
+
9159
+ function renderTraceCommandInput(entry, inputText, label) {
9160
+ const value = String(inputText || "").trim();
9161
+ if (!value) return "";
9162
+ return "<div class='trace-tool-input trace-tool-input-command'>"
9163
+ + "<div class='trace-tool-code-label'>" + escapeHtml(label || "Command") + "</div>"
9164
+ + renderTraceOutput(value, entry.id + ":input", { label: label || "Command" })
9165
+ + "</div>";
9166
+ }
9167
+
9168
+ function renderTraceToolInput(entry) {
9169
+ const inputText = String(entry.args || entry.argsSummary || "").trim();
9170
+ if (!inputText) return "";
9171
+ const toolName = String(entry.toolName || "").trim().toLowerCase();
9172
+ if (toolName === "bash") return renderTraceCommandInput(entry, inputText, "Command");
9173
+ if (toolName === "repl_send" || toolName === "studio_repl_send") return renderTraceCommandInput(entry, inputText, "Code");
9174
+ const payload = parseTraceToolArgsObject(inputText);
9175
+ if (payload && toolName === "edit") return renderTraceEditInput(entry, payload, inputText);
9176
+ if (payload && toolName === "write") return renderTraceWriteInput(entry, payload, inputText);
9177
+ if (payload && toolName === "read") return renderTraceReadInput(entry, payload, inputText);
9178
+ return renderTraceOutput(inputText, entry.id + ":input", { label: "Input" });
9179
+ }
9180
+
8987
9181
  function renderTraceImages(images) {
8988
9182
  const normalizedImages = Array.isArray(images)
8989
9183
  ? images.map((image, index) => normalizeTraceImage(image, index)).filter(Boolean)
@@ -9348,9 +9542,9 @@
9348
9542
  }
9349
9543
 
9350
9544
  const title = entry.label || entry.toolName || "tool";
9351
- const inputText = entry.args || entry.argsSummary || "";
9352
- const argsSummary = inputText
9353
- ? "<div class='trace-section trace-section-input'><div class='trace-section-label'>Input</div>" + renderTraceOutput(inputText, entry.id + ":input", { label: "Input" }) + "</div>"
9545
+ const inputHtml = renderTraceToolInput(entry);
9546
+ const argsSummary = inputHtml
9547
+ ? "<div class='trace-section trace-section-input'><div class='trace-section-label'>Input</div>" + inputHtml + "</div>"
9354
9548
  : "";
9355
9549
  const imageOutput = renderTraceImages(entry.images);
9356
9550
  const outputPieces = [];
@@ -9702,7 +9896,222 @@
9702
9896
  }
9703
9897
  }
9704
9898
 
9899
+ function getGitChangesContext() {
9900
+ return getHtmlPreviewResourceContextOptions();
9901
+ }
9902
+
9903
+ function getGitChangedFiles() {
9904
+ return Array.isArray(gitChangesState.files) ? gitChangesState.files : [];
9905
+ }
9906
+
9907
+ function getSelectedGitChangedFile() {
9908
+ const files = getGitChangedFiles();
9909
+ if (!files.length) return null;
9910
+ const selectedPath = String(gitChangesState.selectedPath || "");
9911
+ return files.find((file) => String(file.path || "") === selectedPath) || files[0] || null;
9912
+ }
9913
+
9914
+ function getGitChangeStatusLabel(status) {
9915
+ if (status === "untracked") return "Untracked";
9916
+ if (status === "added") return "Added";
9917
+ if (status === "deleted") return "Deleted";
9918
+ if (status === "renamed") return "Renamed";
9919
+ if (status === "binary") return "Binary";
9920
+ return "Modified";
9921
+ }
9922
+
9923
+ function getGitChangeStatusIcon(status) {
9924
+ if (status === "untracked") return "??";
9925
+ if (status === "added") return "A";
9926
+ if (status === "deleted") return "D";
9927
+ if (status === "renamed") return "R";
9928
+ if (status === "binary") return "BIN";
9929
+ return "M";
9930
+ }
9931
+
9932
+ function buildGitChangesDiffHtml(diffText) {
9933
+ const lines = String(diffText || "").split("\n");
9934
+ return "<pre class='git-changes-diff'><code>" + lines.map((line) => {
9935
+ let cls = "git-changes-line";
9936
+ if (line.startsWith("+++") || line.startsWith("---") || line.startsWith("diff --git") || line.startsWith("@@")) {
9937
+ cls += " git-changes-line-meta";
9938
+ } else if (line.startsWith("+")) {
9939
+ cls += " git-changes-line-add";
9940
+ } else if (line.startsWith("-")) {
9941
+ cls += " git-changes-line-del";
9942
+ }
9943
+ return "<span class='" + cls + "'>" + escapeHtml(line || " ") + "</span>";
9944
+ }).join("\n") + "</code></pre>";
9945
+ }
9946
+
9947
+ function buildGitChangesPanelHtml() {
9948
+ const files = getGitChangedFiles();
9949
+ const selected = getSelectedGitChangedFile();
9950
+ const isLoading = gitChangesState.status === "loading";
9951
+ const hasError = gitChangesState.status === "error";
9952
+ const label = gitChangesState.label || (files.length ? (files.length + " changed") : "Git changes");
9953
+ const branch = gitChangesState.branch || "";
9954
+ const repoRoot = gitChangesState.repoRoot || "";
9955
+ const subtitleParts = [];
9956
+ subtitleParts.push(label);
9957
+ if (branch) subtitleParts.push((gitChangesState.hasHead === false ? "No commits yet on " : "on ") + branch);
9958
+ const rows = files.length
9959
+ ? files.map((file) => {
9960
+ const path = String(file.path || "");
9961
+ const status = String(file.status || "modified");
9962
+ const isSelected = selected && String(selected.path || "") === path;
9963
+ const stats = (Number(file.additions) || 0) || (Number(file.deletions) || 0)
9964
+ ? "<span class='git-changes-stats'><span class='git-changes-additions'>+" + escapeHtml(String(Number(file.additions) || 0)) + "</span><span class='git-changes-deletions'>−" + escapeHtml(String(Number(file.deletions) || 0)) + "</span></span>"
9965
+ : "";
9966
+ return "<button type='button' class='git-changes-file" + (isSelected ? " is-selected" : "") + "' data-git-change-action='select' data-git-change-path='" + escapeHtml(path) + "' title='" + escapeHtml(path) + "'>"
9967
+ + "<span class='git-changes-file-icon git-changes-file-icon-" + escapeHtml(status) + "'>" + escapeHtml(getGitChangeStatusIcon(status)) + "</span>"
9968
+ + "<span class='git-changes-file-name'>" + escapeHtml(path) + "</span>"
9969
+ + "<span class='git-changes-file-status'>" + escapeHtml(getGitChangeStatusLabel(status)) + "</span>"
9970
+ + stats
9971
+ + "</button>";
9972
+ }).join("")
9973
+ : "<div class='git-changes-empty'>" + escapeHtml(isLoading ? "Loading git changes…" : (gitChangesState.message || "No uncommitted git changes.")) + "</div>";
9974
+ const selectedDiff = selected && selected.diff ? String(selected.diff) : String(gitChangesState.content || "");
9975
+ const selectedStatus = selected ? String(selected.status || "modified") : "";
9976
+ const selectedCanOpen = selected && selectedStatus !== "deleted" && gitChangesState.repoRoot;
9977
+ const selectedAbsPath = selectedCanOpen ? String(gitChangesState.repoRoot).replace(/\/$/, "") + "/" + String(selected.path || "") : "";
9978
+ const notice = hasError && gitChangesState.message
9979
+ ? "<div class='git-changes-notice git-changes-notice-" + escapeHtml(gitChangesState.level || "warning") + "'>" + escapeHtml(gitChangesState.message) + "</div>"
9980
+ : "";
9981
+ return "<div class='git-changes-panel'>"
9982
+ + "<div class='git-changes-toolbar'>"
9983
+ + "<div class='git-changes-title-group'>"
9984
+ + "<div class='git-changes-title'>Git changes</div>"
9985
+ + "<div class='git-changes-subtitle'>" + escapeHtml(subtitleParts.filter(Boolean).join(" · ")) + "</div>"
9986
+ + (repoRoot ? "<div class='git-changes-root' title='" + escapeHtml(repoRoot) + "'>" + escapeHtml(repoRoot) + "</div>" : "")
9987
+ + "</div>"
9988
+ + "<div class='git-changes-actions'>"
9989
+ + "<button type='button' data-git-change-action='refresh'" + (isLoading ? " disabled" : "") + ">Refresh</button>"
9990
+ + "<button type='button' data-git-change-action='open' data-git-change-abs-path='" + escapeHtml(selectedAbsPath) + "'" + (selectedCanOpen ? "" : " disabled") + ">Open file</button>"
9991
+ + "<button type='button' data-git-change-action='load'" + (gitChangesState.content ? "" : " disabled") + ">Load diff</button>"
9992
+ + "<button type='button' data-git-change-action='copy'" + (gitChangesState.content ? "" : " disabled") + ">Copy diff</button>"
9993
+ + "</div>"
9994
+ + "</div>"
9995
+ + notice
9996
+ + "<div class='git-changes-body'>"
9997
+ + "<div class='git-changes-file-list' role='list'>" + rows + "</div>"
9998
+ + "<div class='git-changes-diff-pane'>" + (selectedDiff ? buildGitChangesDiffHtml(selectedDiff) : "<div class='git-changes-empty'>Select a changed file.</div>") + "</div>"
9999
+ + "</div>"
10000
+ + "</div>";
10001
+ }
10002
+
10003
+ function getGitChangesScrollSnapshot() {
10004
+ if (!critiqueViewEl) return null;
10005
+ const fileListEl = critiqueViewEl.querySelector(".git-changes-file-list");
10006
+ const diffPaneEl = critiqueViewEl.querySelector(".git-changes-diff-pane");
10007
+ return {
10008
+ paneTop: critiqueViewEl.scrollTop || 0,
10009
+ paneLeft: critiqueViewEl.scrollLeft || 0,
10010
+ fileListTop: fileListEl ? fileListEl.scrollTop || 0 : 0,
10011
+ fileListLeft: fileListEl ? fileListEl.scrollLeft || 0 : 0,
10012
+ diffTop: diffPaneEl ? diffPaneEl.scrollTop || 0 : 0,
10013
+ diffLeft: diffPaneEl ? diffPaneEl.scrollLeft || 0 : 0,
10014
+ };
10015
+ }
10016
+
10017
+ function restoreGitChangesScrollSnapshot(snapshot, options) {
10018
+ if (!critiqueViewEl || !snapshot) return;
10019
+ const fileListEl = critiqueViewEl.querySelector(".git-changes-file-list");
10020
+ const diffPaneEl = critiqueViewEl.querySelector(".git-changes-diff-pane");
10021
+ critiqueViewEl.scrollTop = snapshot.paneTop || 0;
10022
+ critiqueViewEl.scrollLeft = snapshot.paneLeft || 0;
10023
+ if (fileListEl) {
10024
+ fileListEl.scrollTop = snapshot.fileListTop || 0;
10025
+ fileListEl.scrollLeft = snapshot.fileListLeft || 0;
10026
+ }
10027
+ if (diffPaneEl) {
10028
+ if (options && options.resetDiffScroll) {
10029
+ diffPaneEl.scrollTop = 0;
10030
+ diffPaneEl.scrollLeft = 0;
10031
+ } else {
10032
+ diffPaneEl.scrollTop = snapshot.diffTop || 0;
10033
+ diffPaneEl.scrollLeft = snapshot.diffLeft || 0;
10034
+ }
10035
+ }
10036
+ }
10037
+
10038
+ function renderGitChangesView(options) {
10039
+ if (!critiqueViewEl) return;
10040
+ const scrollSnapshot = options && options.preserveScroll ? getGitChangesScrollSnapshot() : null;
10041
+ finishPreviewRender(critiqueViewEl);
10042
+ critiqueViewEl.classList.add("git-changes-host");
10043
+ critiqueViewEl.innerHTML = buildGitChangesPanelHtml();
10044
+ critiqueViewEl.classList.remove("response-scroll-resetting");
10045
+ restoreGitChangesScrollSnapshot(scrollSnapshot, options || {});
10046
+ if (gitChangesState.status === "idle") requestGitChangesSnapshot({ preserveScroll: true });
10047
+ scheduleResponsePaneRepaintNudge();
10048
+ }
10049
+
10050
+ function requestGitChangesSnapshot(options) {
10051
+ const requestId = makeRequestId();
10052
+ const context = getGitChangesContext();
10053
+ gitChangesState = {
10054
+ ...gitChangesState,
10055
+ status: "loading",
10056
+ requestId,
10057
+ message: "",
10058
+ level: "info",
10059
+ };
10060
+ if (rightView === "changes") renderGitChangesView({ preserveScroll: Boolean(options && options.preserveScroll) });
10061
+ const message = { type: "git_changes_request", requestId };
10062
+ if (context.sourcePath) message.sourcePath = context.sourcePath;
10063
+ if (context.resourceDir) message.resourceDir = context.resourceDir;
10064
+ if (!sendMessage(message)) {
10065
+ gitChangesState = { ...gitChangesState, status: "error", message: "Studio is not connected.", level: "error" };
10066
+ if (rightView === "changes") renderGitChangesView({ preserveScroll: true });
10067
+ } else if (options && options.user) {
10068
+ setStatus("Refreshing git changes…", "warning");
10069
+ }
10070
+ }
10071
+
10072
+ async function handleGitChangesPaneClick(event) {
10073
+ if (rightView !== "changes") return;
10074
+ const target = event.target;
10075
+ const actionEl = target instanceof Element ? target.closest("[data-git-change-action]") : null;
10076
+ if (!actionEl) return;
10077
+ event.preventDefault();
10078
+ const action = actionEl.getAttribute("data-git-change-action") || "";
10079
+ if (action === "select") {
10080
+ gitChangesState = { ...gitChangesState, selectedPath: actionEl.getAttribute("data-git-change-path") || "" };
10081
+ renderGitChangesView({ preserveScroll: true, resetDiffScroll: true });
10082
+ return;
10083
+ }
10084
+ if (action === "refresh") {
10085
+ requestGitChangesSnapshot({ user: true, preserveScroll: true });
10086
+ return;
10087
+ }
10088
+ if (action === "copy") {
10089
+ const ok = await writeTextToClipboard(String(gitChangesState.content || ""));
10090
+ setStatus(ok ? "Copied git diff." : "Clipboard write failed.", ok ? "success" : "warning");
10091
+ return;
10092
+ }
10093
+ if (action === "load") {
10094
+ if (!String(gitChangesState.content || "").trim()) {
10095
+ setStatus("No git diff to load.", "warning");
10096
+ return;
10097
+ }
10098
+ setEditorText(String(gitChangesState.content || ""), { preserveScroll: false, preserveSelection: false });
10099
+ setSourceState({ source: "blank", label: gitChangesState.label || "git diff", path: null });
10100
+ setEditorLanguage("diff");
10101
+ setStatus("Loaded current git diff into editor.", "success");
10102
+ return;
10103
+ }
10104
+ if (action === "open") {
10105
+ const absPath = actionEl.getAttribute("data-git-change-abs-path") || "";
10106
+ if (!absPath) return;
10107
+ await openPreviewDocumentHere(absPath, getFileBrowserLocalLinkContext(), { fallbackPath: absPath, fileBackedIntent: true });
10108
+ ensureCurrentEditorFileBackedFromFilesPath(absPath);
10109
+ setStatus("Opened changed file in editor.", "success");
10110
+ }
10111
+ }
10112
+
9705
10113
  function renderActiveResult() {
10114
+ if (critiqueViewEl) critiqueViewEl.classList.toggle("git-changes-host", rightView === "changes");
9706
10115
  if (rightView === "trace") {
9707
10116
  renderTraceView();
9708
10117
  return;
@@ -9718,6 +10127,11 @@
9718
10127
  return;
9719
10128
  }
9720
10129
 
10130
+ if (rightView === "changes") {
10131
+ renderGitChangesView();
10132
+ return;
10133
+ }
10134
+
9721
10135
  if (rightView === "editor-preview") {
9722
10136
  const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
9723
10137
  if (!editorText.trim()) {
@@ -9796,7 +10210,7 @@
9796
10210
  : normalizeForCompare(sourceTextEl.value);
9797
10211
  const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
9798
10212
  const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
9799
- const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl" || rightView === "files";
10213
+ const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl" || rightView === "files" || rightView === "changes";
9800
10214
 
9801
10215
  if (responseWrapEl) {
9802
10216
  responseWrapEl.hidden = showingAuxiliaryRightPane;
@@ -9839,6 +10253,8 @@
9839
10253
  exportPdfBtn.title = "Working view does not support preview export.";
9840
10254
  } else if (rightView === "files") {
9841
10255
  exportPdfBtn.title = "Files view does not support preview export.";
10256
+ } else if (rightView === "changes") {
10257
+ exportPdfBtn.title = "Changes view does not support preview export.";
9842
10258
  } else if (exportingReplJournal && !replJournalExportEntries.length) {
9843
10259
  exportPdfBtn.title = "No Studio REPL record entries to export for this session yet.";
9844
10260
  } else if (rightView === "markdown") {
@@ -10018,7 +10434,6 @@
10018
10434
  if (clearWorkspaceBtn) clearWorkspaceBtn.disabled = uiBusy;
10019
10435
  sendEditorBtn.disabled = uiBusy || isEditorOnlyMode;
10020
10436
  if (getEditorBtn) getEditorBtn.disabled = uiBusy;
10021
- if (loadGitDiffBtn) loadGitDiffBtn.disabled = uiBusy;
10022
10437
  syncRunAndCritiqueButtons();
10023
10438
  copyDraftBtn.disabled = uiBusy;
10024
10439
  if (suggestCompletionBtn) {
@@ -10775,6 +11190,9 @@
10775
11190
  if (rightView === "trace" && previousView !== "trace") {
10776
11191
  traceAutoScroll = true;
10777
11192
  }
11193
+ if (rightView === "changes" && previousView !== "changes" && gitChangesState.status === "idle") {
11194
+ requestGitChangesSnapshot();
11195
+ }
10778
11196
  if (rightView === "repl" && previousView !== "repl") {
10779
11197
  replFollow = true;
10780
11198
  startReplPolling();
@@ -13097,17 +13515,24 @@
13097
13515
  if (!scratchpadRecentPanelEl) return;
13098
13516
  scratchpadRecentPanelEl.hidden = !scratchpadRecentVisible;
13099
13517
  if (!scratchpadRecentVisible) return;
13518
+ const headerHtml = "<div class='scratchpad-recent-header'>"
13519
+ + "<div class='scratchpad-recent-heading-group'>"
13520
+ + "<div class='scratchpad-recent-heading'>Recent scratchpads</div>"
13521
+ + "<div class='scratchpad-recent-subtitle'>Load, append, or copy notes saved for other documents and drafts.</div>"
13522
+ + "</div>"
13523
+ + "<button type='button' class='scratchpad-recent-hide-btn' data-scratchpad-recent-action='hide' aria-label='Hide recent scratchpads' title='Hide recent scratchpads'>Hide</button>"
13524
+ + "</div>";
13100
13525
  if (scratchpadRecentLoading) {
13101
- scratchpadRecentPanelEl.innerHTML = "<div class='scratchpad-recent-loading'>Loading recent scratchpads…</div>";
13526
+ scratchpadRecentPanelEl.innerHTML = headerHtml + "<div class='scratchpad-recent-loading'>Loading recent scratchpads…</div>";
13102
13527
  return;
13103
13528
  }
13104
13529
  const currentKey = getCurrentStudioDocumentDescriptor().key;
13105
13530
  const entries = Array.isArray(scratchpadRecentEntries) ? scratchpadRecentEntries : [];
13106
13531
  if (!entries.length) {
13107
- scratchpadRecentPanelEl.innerHTML = "<div class='scratchpad-recent-empty'>No other saved scratchpads yet.</div>";
13532
+ scratchpadRecentPanelEl.innerHTML = headerHtml + "<div class='scratchpad-recent-empty'>No other saved scratchpads yet.</div>";
13108
13533
  return;
13109
13534
  }
13110
- scratchpadRecentPanelEl.innerHTML = "<div class='scratchpad-recent-list'>" + entries.map((entry) => {
13535
+ scratchpadRecentPanelEl.innerHTML = headerHtml + "<div class='scratchpad-recent-list'>" + entries.map((entry) => {
13111
13536
  const key = String(entry && entry.documentKey ? entry.documentKey : "");
13112
13537
  const isCurrent = key === currentKey;
13113
13538
  const label = String(entry && entry.label ? entry.label : key || "scratchpad");
@@ -13145,13 +13570,19 @@
13145
13570
  }
13146
13571
  }
13147
13572
 
13573
+ function hideScratchpadRecentPanel() {
13574
+ scratchpadRecentVisible = false;
13575
+ renderScratchpadRecentPanel();
13576
+ updateScratchpadUi();
13577
+ }
13578
+
13148
13579
  function toggleScratchpadRecentPanel() {
13149
- scratchpadRecentVisible = !scratchpadRecentVisible;
13150
13580
  if (scratchpadRecentVisible) {
13151
- void loadScratchpadRecentEntries();
13152
- } else {
13153
- renderScratchpadRecentPanel();
13581
+ hideScratchpadRecentPanel();
13582
+ return;
13154
13583
  }
13584
+ scratchpadRecentVisible = true;
13585
+ void loadScratchpadRecentEntries();
13155
13586
  updateScratchpadUi();
13156
13587
  }
13157
13588
 
@@ -13181,6 +13612,7 @@
13181
13612
  if (!confirmed) return;
13182
13613
  }
13183
13614
  setScratchpadText(text);
13615
+ hideScratchpadRecentPanel();
13184
13616
  setStatus("Loaded recent scratchpad into current scratchpad.", "success");
13185
13617
  } catch (error) {
13186
13618
  setStatus("Could not use recent scratchpad: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
@@ -16864,7 +17296,7 @@
16864
17296
  const hasNotes = count > 0;
16865
17297
  const isOpen = isReviewNotesOpen();
16866
17298
  if (reviewNotesBtn) {
16867
- reviewNotesBtn.textContent = hasNotes ? "Comments" : "Comments";
17299
+ reviewNotesBtn.textContent = "Comments";
16868
17300
  reviewNotesBtn.classList.toggle("has-content", hasNotes);
16869
17301
  reviewNotesBtn.classList.toggle("is-active", isOpen);
16870
17302
  reviewNotesBtn.setAttribute("aria-pressed", isOpen ? "true" : "false");
@@ -17466,7 +17898,7 @@
17466
17898
  const hasContent = Boolean(normalized.trim());
17467
17899
  const descriptor = getCurrentStudioDocumentDescriptor();
17468
17900
  if (scratchpadBtn) {
17469
- scratchpadBtn.textContent = hasContent ? "Scratchpad" : "Scratchpad";
17901
+ scratchpadBtn.textContent = "Scratchpad";
17470
17902
  scratchpadBtn.classList.toggle("has-content", hasContent);
17471
17903
  scratchpadBtn.title = hasContent
17472
17904
  ? ("Open the local persistent scratchpad for this document/draft. Scope: " + descriptor.label + ". File-backed docs come back across Pi restarts; unsaved drafts stay with this draft instance until saved or cleared.")
@@ -17480,6 +17912,9 @@
17480
17912
  if (scratchpadRecentBtn) {
17481
17913
  scratchpadRecentBtn.textContent = scratchpadRecentVisible ? "Hide recent" : "Recent…";
17482
17914
  scratchpadRecentBtn.setAttribute("aria-expanded", scratchpadRecentVisible ? "true" : "false");
17915
+ scratchpadRecentBtn.title = scratchpadRecentVisible
17916
+ ? "Hide recent scratchpads."
17917
+ : "Show recent non-empty scratchpads saved for other files and drafts.";
17483
17918
  }
17484
17919
  if (scratchpadInsertBtn) scratchpadInsertBtn.disabled = !hasContent;
17485
17920
  if (scratchpadCopyBtn) scratchpadCopyBtn.disabled = !hasContent;
@@ -18571,28 +19006,31 @@
18571
19006
  return;
18572
19007
  }
18573
19008
 
18574
- if (message.type === "git_diff_snapshot") {
18575
- if (typeof message.requestId === "string" && pendingRequestId === message.requestId) {
18576
- pendingRequestId = null;
18577
- pendingKind = null;
18578
- }
18579
-
18580
- const content = typeof message.content === "string" ? message.content : "";
18581
- const label = typeof message.label === "string" && message.label.trim()
18582
- ? message.label.trim()
18583
- : "git diff";
18584
- setEditorText(content, { preserveScroll: false, preserveSelection: false });
18585
- setSourceState({ source: "blank", label, path: null });
18586
- setEditorLanguage("diff");
18587
- setBusy(false);
18588
- setWsState("Ready");
18589
- refreshResponseUi();
18590
- setStatus(
18591
- typeof message.message === "string" && message.message.trim()
18592
- ? message.message
18593
- : "Loaded current git diff.",
18594
- "success",
18595
- );
19009
+ if (message.type === "git_changes_snapshot") {
19010
+ const requestId = typeof message.requestId === "string" ? message.requestId : "";
19011
+ const preserveScroll = Boolean(gitChangesState.requestId && requestId && requestId === gitChangesState.requestId);
19012
+ if (requestId && gitChangesState.requestId && requestId !== gitChangesState.requestId) return;
19013
+ const ok = message.ok !== false;
19014
+ const files = Array.isArray(message.files) ? message.files : [];
19015
+ const selectedPath = files.some((file) => String(file && file.path || "") === String(gitChangesState.selectedPath || ""))
19016
+ ? gitChangesState.selectedPath
19017
+ : (files[0] && files[0].path ? String(files[0].path) : "");
19018
+ gitChangesState = {
19019
+ status: ok ? "ready" : "error",
19020
+ requestId: null,
19021
+ content: ok && typeof message.content === "string" ? message.content : "",
19022
+ label: ok && typeof message.label === "string" ? message.label : "",
19023
+ repoRoot: ok && typeof message.repoRoot === "string" ? message.repoRoot : "",
19024
+ branch: ok && typeof message.branch === "string" ? message.branch : "",
19025
+ hasHead: ok ? message.hasHead !== false : true,
19026
+ files,
19027
+ selectedPath,
19028
+ message: typeof message.message === "string" ? message.message : "",
19029
+ level: typeof message.level === "string" ? message.level : "info",
19030
+ };
19031
+ if (rightView === "changes") renderGitChangesView({ preserveScroll });
19032
+ if (ok) setStatus(files.length ? "Loaded git changes." : "No uncommitted git changes.", files.length ? "success" : "warning");
19033
+ else setStatus(gitChangesState.message || "Could not load git changes.", gitChangesState.level === "error" ? "error" : "warning");
18596
19034
  return;
18597
19035
  }
18598
19036
 
@@ -19248,7 +19686,7 @@
19248
19686
  if (historyPrevBtn) {
19249
19687
  historyPrevBtn.addEventListener("click", () => {
19250
19688
  if (!responseHistory.length) {
19251
- setStatus("No response history available yet.", "warning");
19689
+ setStatus("No branch history available yet.", "warning");
19252
19690
  return;
19253
19691
  }
19254
19692
  selectHistoryIndex(responseHistoryIndex - 1);
@@ -19258,7 +19696,7 @@
19258
19696
  if (historyNextBtn) {
19259
19697
  historyNextBtn.addEventListener("click", () => {
19260
19698
  if (!responseHistory.length) {
19261
- setStatus("No response history available yet.", "warning");
19699
+ setStatus("No branch history available yet.", "warning");
19262
19700
  return;
19263
19701
  }
19264
19702
  selectHistoryIndex(responseHistoryIndex + 1);
@@ -19268,7 +19706,7 @@
19268
19706
  if (historyLastBtn) {
19269
19707
  historyLastBtn.addEventListener("click", () => {
19270
19708
  if (!responseHistory.length) {
19271
- setStatus("No response history available yet.", "warning");
19709
+ setStatus("No branch history available yet.", "warning");
19272
19710
  return;
19273
19711
  }
19274
19712
  selectHistoryIndex(responseHistory.length - 1);
@@ -19295,7 +19733,7 @@
19295
19733
  if (responseHistory.length > 0) {
19296
19734
  selectHistoryIndex(responseHistory.length - 1, { silent: true });
19297
19735
  queuedLatestResponse = null;
19298
- setStatus("Pulled latest response from history.", "success");
19736
+ setStatus("Pulled latest response from branch history.", "success");
19299
19737
  updateResultActionButtons();
19300
19738
  } else if (applyLatestPayload(queuedLatestResponse)) {
19301
19739
  queuedLatestResponse = null;
@@ -19653,27 +20091,6 @@
19653
20091
  });
19654
20092
  }
19655
20093
 
19656
- if (loadGitDiffBtn) {
19657
- loadGitDiffBtn.addEventListener("click", () => {
19658
- const requestId = beginUiAction("load_git_diff");
19659
- if (!requestId) return;
19660
-
19661
- const effectivePath = getEffectiveSavePath();
19662
- const sent = sendMessage({
19663
- type: "load_git_diff_request",
19664
- requestId,
19665
- sourcePath: effectivePath || sourceState.path || undefined,
19666
- resourceDir: getCurrentResourceDirValue() || undefined,
19667
- });
19668
-
19669
- if (!sent) {
19670
- pendingRequestId = null;
19671
- pendingKind = null;
19672
- setBusy(false);
19673
- }
19674
- });
19675
- }
19676
-
19677
20094
  if (zenModeBtn) {
19678
20095
  zenModeBtn.addEventListener("click", () => {
19679
20096
  setStudioZenMode(!studioZenModeEnabled);
@@ -20039,7 +20456,12 @@
20039
20456
  const actionEl = target instanceof Element ? target.closest("[data-scratchpad-recent-action]") : null;
20040
20457
  if (!actionEl) return;
20041
20458
  event.preventDefault();
20459
+ event.stopPropagation();
20042
20460
  const action = String(actionEl.getAttribute("data-scratchpad-recent-action") || "load");
20461
+ if (action === "hide") {
20462
+ hideScratchpadRecentPanel();
20463
+ return;
20464
+ }
20043
20465
  const key = String(actionEl.getAttribute("data-scratchpad-key") || "");
20044
20466
  void applyScratchpadRecentAction(action, key);
20045
20467
  });