pi-studio 0.9.23 → 0.9.24

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
 
@@ -9702,7 +9736,222 @@
9702
9736
  }
9703
9737
  }
9704
9738
 
9739
+ function getGitChangesContext() {
9740
+ return getHtmlPreviewResourceContextOptions();
9741
+ }
9742
+
9743
+ function getGitChangedFiles() {
9744
+ return Array.isArray(gitChangesState.files) ? gitChangesState.files : [];
9745
+ }
9746
+
9747
+ function getSelectedGitChangedFile() {
9748
+ const files = getGitChangedFiles();
9749
+ if (!files.length) return null;
9750
+ const selectedPath = String(gitChangesState.selectedPath || "");
9751
+ return files.find((file) => String(file.path || "") === selectedPath) || files[0] || null;
9752
+ }
9753
+
9754
+ function getGitChangeStatusLabel(status) {
9755
+ if (status === "untracked") return "Untracked";
9756
+ if (status === "added") return "Added";
9757
+ if (status === "deleted") return "Deleted";
9758
+ if (status === "renamed") return "Renamed";
9759
+ if (status === "binary") return "Binary";
9760
+ return "Modified";
9761
+ }
9762
+
9763
+ function getGitChangeStatusIcon(status) {
9764
+ if (status === "untracked") return "??";
9765
+ if (status === "added") return "A";
9766
+ if (status === "deleted") return "D";
9767
+ if (status === "renamed") return "R";
9768
+ if (status === "binary") return "BIN";
9769
+ return "M";
9770
+ }
9771
+
9772
+ function buildGitChangesDiffHtml(diffText) {
9773
+ const lines = String(diffText || "").split("\n");
9774
+ return "<pre class='git-changes-diff'><code>" + lines.map((line) => {
9775
+ let cls = "git-changes-line";
9776
+ if (line.startsWith("+++") || line.startsWith("---") || line.startsWith("diff --git") || line.startsWith("@@")) {
9777
+ cls += " git-changes-line-meta";
9778
+ } else if (line.startsWith("+")) {
9779
+ cls += " git-changes-line-add";
9780
+ } else if (line.startsWith("-")) {
9781
+ cls += " git-changes-line-del";
9782
+ }
9783
+ return "<span class='" + cls + "'>" + escapeHtml(line || " ") + "</span>";
9784
+ }).join("\n") + "</code></pre>";
9785
+ }
9786
+
9787
+ function buildGitChangesPanelHtml() {
9788
+ const files = getGitChangedFiles();
9789
+ const selected = getSelectedGitChangedFile();
9790
+ const isLoading = gitChangesState.status === "loading";
9791
+ const hasError = gitChangesState.status === "error";
9792
+ const label = gitChangesState.label || (files.length ? (files.length + " changed") : "Git changes");
9793
+ const branch = gitChangesState.branch || "";
9794
+ const repoRoot = gitChangesState.repoRoot || "";
9795
+ const subtitleParts = [];
9796
+ subtitleParts.push(label);
9797
+ if (branch) subtitleParts.push((gitChangesState.hasHead === false ? "No commits yet on " : "on ") + branch);
9798
+ const rows = files.length
9799
+ ? files.map((file) => {
9800
+ const path = String(file.path || "");
9801
+ const status = String(file.status || "modified");
9802
+ const isSelected = selected && String(selected.path || "") === path;
9803
+ const stats = (Number(file.additions) || 0) || (Number(file.deletions) || 0)
9804
+ ? "<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>"
9805
+ : "";
9806
+ 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) + "'>"
9807
+ + "<span class='git-changes-file-icon git-changes-file-icon-" + escapeHtml(status) + "'>" + escapeHtml(getGitChangeStatusIcon(status)) + "</span>"
9808
+ + "<span class='git-changes-file-name'>" + escapeHtml(path) + "</span>"
9809
+ + "<span class='git-changes-file-status'>" + escapeHtml(getGitChangeStatusLabel(status)) + "</span>"
9810
+ + stats
9811
+ + "</button>";
9812
+ }).join("")
9813
+ : "<div class='git-changes-empty'>" + escapeHtml(isLoading ? "Loading git changes…" : (gitChangesState.message || "No uncommitted git changes.")) + "</div>";
9814
+ const selectedDiff = selected && selected.diff ? String(selected.diff) : String(gitChangesState.content || "");
9815
+ const selectedStatus = selected ? String(selected.status || "modified") : "";
9816
+ const selectedCanOpen = selected && selectedStatus !== "deleted" && gitChangesState.repoRoot;
9817
+ const selectedAbsPath = selectedCanOpen ? String(gitChangesState.repoRoot).replace(/\/$/, "") + "/" + String(selected.path || "") : "";
9818
+ const notice = hasError && gitChangesState.message
9819
+ ? "<div class='git-changes-notice git-changes-notice-" + escapeHtml(gitChangesState.level || "warning") + "'>" + escapeHtml(gitChangesState.message) + "</div>"
9820
+ : "";
9821
+ return "<div class='git-changes-panel'>"
9822
+ + "<div class='git-changes-toolbar'>"
9823
+ + "<div class='git-changes-title-group'>"
9824
+ + "<div class='git-changes-title'>Git changes</div>"
9825
+ + "<div class='git-changes-subtitle'>" + escapeHtml(subtitleParts.filter(Boolean).join(" · ")) + "</div>"
9826
+ + (repoRoot ? "<div class='git-changes-root' title='" + escapeHtml(repoRoot) + "'>" + escapeHtml(repoRoot) + "</div>" : "")
9827
+ + "</div>"
9828
+ + "<div class='git-changes-actions'>"
9829
+ + "<button type='button' data-git-change-action='refresh'" + (isLoading ? " disabled" : "") + ">Refresh</button>"
9830
+ + "<button type='button' data-git-change-action='open' data-git-change-abs-path='" + escapeHtml(selectedAbsPath) + "'" + (selectedCanOpen ? "" : " disabled") + ">Open file</button>"
9831
+ + "<button type='button' data-git-change-action='load'" + (gitChangesState.content ? "" : " disabled") + ">Load diff</button>"
9832
+ + "<button type='button' data-git-change-action='copy'" + (gitChangesState.content ? "" : " disabled") + ">Copy diff</button>"
9833
+ + "</div>"
9834
+ + "</div>"
9835
+ + notice
9836
+ + "<div class='git-changes-body'>"
9837
+ + "<div class='git-changes-file-list' role='list'>" + rows + "</div>"
9838
+ + "<div class='git-changes-diff-pane'>" + (selectedDiff ? buildGitChangesDiffHtml(selectedDiff) : "<div class='git-changes-empty'>Select a changed file.</div>") + "</div>"
9839
+ + "</div>"
9840
+ + "</div>";
9841
+ }
9842
+
9843
+ function getGitChangesScrollSnapshot() {
9844
+ if (!critiqueViewEl) return null;
9845
+ const fileListEl = critiqueViewEl.querySelector(".git-changes-file-list");
9846
+ const diffPaneEl = critiqueViewEl.querySelector(".git-changes-diff-pane");
9847
+ return {
9848
+ paneTop: critiqueViewEl.scrollTop || 0,
9849
+ paneLeft: critiqueViewEl.scrollLeft || 0,
9850
+ fileListTop: fileListEl ? fileListEl.scrollTop || 0 : 0,
9851
+ fileListLeft: fileListEl ? fileListEl.scrollLeft || 0 : 0,
9852
+ diffTop: diffPaneEl ? diffPaneEl.scrollTop || 0 : 0,
9853
+ diffLeft: diffPaneEl ? diffPaneEl.scrollLeft || 0 : 0,
9854
+ };
9855
+ }
9856
+
9857
+ function restoreGitChangesScrollSnapshot(snapshot, options) {
9858
+ if (!critiqueViewEl || !snapshot) return;
9859
+ const fileListEl = critiqueViewEl.querySelector(".git-changes-file-list");
9860
+ const diffPaneEl = critiqueViewEl.querySelector(".git-changes-diff-pane");
9861
+ critiqueViewEl.scrollTop = snapshot.paneTop || 0;
9862
+ critiqueViewEl.scrollLeft = snapshot.paneLeft || 0;
9863
+ if (fileListEl) {
9864
+ fileListEl.scrollTop = snapshot.fileListTop || 0;
9865
+ fileListEl.scrollLeft = snapshot.fileListLeft || 0;
9866
+ }
9867
+ if (diffPaneEl) {
9868
+ if (options && options.resetDiffScroll) {
9869
+ diffPaneEl.scrollTop = 0;
9870
+ diffPaneEl.scrollLeft = 0;
9871
+ } else {
9872
+ diffPaneEl.scrollTop = snapshot.diffTop || 0;
9873
+ diffPaneEl.scrollLeft = snapshot.diffLeft || 0;
9874
+ }
9875
+ }
9876
+ }
9877
+
9878
+ function renderGitChangesView(options) {
9879
+ if (!critiqueViewEl) return;
9880
+ const scrollSnapshot = options && options.preserveScroll ? getGitChangesScrollSnapshot() : null;
9881
+ finishPreviewRender(critiqueViewEl);
9882
+ critiqueViewEl.classList.add("git-changes-host");
9883
+ critiqueViewEl.innerHTML = buildGitChangesPanelHtml();
9884
+ critiqueViewEl.classList.remove("response-scroll-resetting");
9885
+ restoreGitChangesScrollSnapshot(scrollSnapshot, options || {});
9886
+ if (gitChangesState.status === "idle") requestGitChangesSnapshot({ preserveScroll: true });
9887
+ scheduleResponsePaneRepaintNudge();
9888
+ }
9889
+
9890
+ function requestGitChangesSnapshot(options) {
9891
+ const requestId = makeRequestId();
9892
+ const context = getGitChangesContext();
9893
+ gitChangesState = {
9894
+ ...gitChangesState,
9895
+ status: "loading",
9896
+ requestId,
9897
+ message: "",
9898
+ level: "info",
9899
+ };
9900
+ if (rightView === "changes") renderGitChangesView({ preserveScroll: Boolean(options && options.preserveScroll) });
9901
+ const message = { type: "git_changes_request", requestId };
9902
+ if (context.sourcePath) message.sourcePath = context.sourcePath;
9903
+ if (context.resourceDir) message.resourceDir = context.resourceDir;
9904
+ if (!sendMessage(message)) {
9905
+ gitChangesState = { ...gitChangesState, status: "error", message: "Studio is not connected.", level: "error" };
9906
+ if (rightView === "changes") renderGitChangesView({ preserveScroll: true });
9907
+ } else if (options && options.user) {
9908
+ setStatus("Refreshing git changes…", "warning");
9909
+ }
9910
+ }
9911
+
9912
+ async function handleGitChangesPaneClick(event) {
9913
+ if (rightView !== "changes") return;
9914
+ const target = event.target;
9915
+ const actionEl = target instanceof Element ? target.closest("[data-git-change-action]") : null;
9916
+ if (!actionEl) return;
9917
+ event.preventDefault();
9918
+ const action = actionEl.getAttribute("data-git-change-action") || "";
9919
+ if (action === "select") {
9920
+ gitChangesState = { ...gitChangesState, selectedPath: actionEl.getAttribute("data-git-change-path") || "" };
9921
+ renderGitChangesView({ preserveScroll: true, resetDiffScroll: true });
9922
+ return;
9923
+ }
9924
+ if (action === "refresh") {
9925
+ requestGitChangesSnapshot({ user: true, preserveScroll: true });
9926
+ return;
9927
+ }
9928
+ if (action === "copy") {
9929
+ const ok = await writeTextToClipboard(String(gitChangesState.content || ""));
9930
+ setStatus(ok ? "Copied git diff." : "Clipboard write failed.", ok ? "success" : "warning");
9931
+ return;
9932
+ }
9933
+ if (action === "load") {
9934
+ if (!String(gitChangesState.content || "").trim()) {
9935
+ setStatus("No git diff to load.", "warning");
9936
+ return;
9937
+ }
9938
+ setEditorText(String(gitChangesState.content || ""), { preserveScroll: false, preserveSelection: false });
9939
+ setSourceState({ source: "blank", label: gitChangesState.label || "git diff", path: null });
9940
+ setEditorLanguage("diff");
9941
+ setStatus("Loaded current git diff into editor.", "success");
9942
+ return;
9943
+ }
9944
+ if (action === "open") {
9945
+ const absPath = actionEl.getAttribute("data-git-change-abs-path") || "";
9946
+ if (!absPath) return;
9947
+ await openPreviewDocumentHere(absPath, getFileBrowserLocalLinkContext(), { fallbackPath: absPath, fileBackedIntent: true });
9948
+ ensureCurrentEditorFileBackedFromFilesPath(absPath);
9949
+ setStatus("Opened changed file in editor.", "success");
9950
+ }
9951
+ }
9952
+
9705
9953
  function renderActiveResult() {
9954
+ if (critiqueViewEl) critiqueViewEl.classList.toggle("git-changes-host", rightView === "changes");
9706
9955
  if (rightView === "trace") {
9707
9956
  renderTraceView();
9708
9957
  return;
@@ -9718,6 +9967,11 @@
9718
9967
  return;
9719
9968
  }
9720
9969
 
9970
+ if (rightView === "changes") {
9971
+ renderGitChangesView();
9972
+ return;
9973
+ }
9974
+
9721
9975
  if (rightView === "editor-preview") {
9722
9976
  const editorText = prepareEditorTextForPreview(sourceTextEl.value || "");
9723
9977
  if (!editorText.trim()) {
@@ -9796,7 +10050,7 @@
9796
10050
  : normalizeForCompare(sourceTextEl.value);
9797
10051
  const responseLoaded = hasResponse && normalizedEditor === latestResponseNormalized;
9798
10052
  const isCritiqueResponse = hasResponse && latestResponseIsStructuredCritique;
9799
- const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl" || rightView === "files";
10053
+ const showingAuxiliaryRightPane = rightView === "trace" || rightView === "repl" || rightView === "files" || rightView === "changes";
9800
10054
 
9801
10055
  if (responseWrapEl) {
9802
10056
  responseWrapEl.hidden = showingAuxiliaryRightPane;
@@ -9839,6 +10093,8 @@
9839
10093
  exportPdfBtn.title = "Working view does not support preview export.";
9840
10094
  } else if (rightView === "files") {
9841
10095
  exportPdfBtn.title = "Files view does not support preview export.";
10096
+ } else if (rightView === "changes") {
10097
+ exportPdfBtn.title = "Changes view does not support preview export.";
9842
10098
  } else if (exportingReplJournal && !replJournalExportEntries.length) {
9843
10099
  exportPdfBtn.title = "No Studio REPL record entries to export for this session yet.";
9844
10100
  } else if (rightView === "markdown") {
@@ -10018,7 +10274,6 @@
10018
10274
  if (clearWorkspaceBtn) clearWorkspaceBtn.disabled = uiBusy;
10019
10275
  sendEditorBtn.disabled = uiBusy || isEditorOnlyMode;
10020
10276
  if (getEditorBtn) getEditorBtn.disabled = uiBusy;
10021
- if (loadGitDiffBtn) loadGitDiffBtn.disabled = uiBusy;
10022
10277
  syncRunAndCritiqueButtons();
10023
10278
  copyDraftBtn.disabled = uiBusy;
10024
10279
  if (suggestCompletionBtn) {
@@ -10775,6 +11030,9 @@
10775
11030
  if (rightView === "trace" && previousView !== "trace") {
10776
11031
  traceAutoScroll = true;
10777
11032
  }
11033
+ if (rightView === "changes" && previousView !== "changes" && gitChangesState.status === "idle") {
11034
+ requestGitChangesSnapshot();
11035
+ }
10778
11036
  if (rightView === "repl" && previousView !== "repl") {
10779
11037
  replFollow = true;
10780
11038
  startReplPolling();
@@ -13097,17 +13355,24 @@
13097
13355
  if (!scratchpadRecentPanelEl) return;
13098
13356
  scratchpadRecentPanelEl.hidden = !scratchpadRecentVisible;
13099
13357
  if (!scratchpadRecentVisible) return;
13358
+ const headerHtml = "<div class='scratchpad-recent-header'>"
13359
+ + "<div class='scratchpad-recent-heading-group'>"
13360
+ + "<div class='scratchpad-recent-heading'>Recent scratchpads</div>"
13361
+ + "<div class='scratchpad-recent-subtitle'>Load, append, or copy notes saved for other documents and drafts.</div>"
13362
+ + "</div>"
13363
+ + "<button type='button' class='scratchpad-recent-hide-btn' data-scratchpad-recent-action='hide' aria-label='Hide recent scratchpads' title='Hide recent scratchpads'>Hide</button>"
13364
+ + "</div>";
13100
13365
  if (scratchpadRecentLoading) {
13101
- scratchpadRecentPanelEl.innerHTML = "<div class='scratchpad-recent-loading'>Loading recent scratchpads…</div>";
13366
+ scratchpadRecentPanelEl.innerHTML = headerHtml + "<div class='scratchpad-recent-loading'>Loading recent scratchpads…</div>";
13102
13367
  return;
13103
13368
  }
13104
13369
  const currentKey = getCurrentStudioDocumentDescriptor().key;
13105
13370
  const entries = Array.isArray(scratchpadRecentEntries) ? scratchpadRecentEntries : [];
13106
13371
  if (!entries.length) {
13107
- scratchpadRecentPanelEl.innerHTML = "<div class='scratchpad-recent-empty'>No other saved scratchpads yet.</div>";
13372
+ scratchpadRecentPanelEl.innerHTML = headerHtml + "<div class='scratchpad-recent-empty'>No other saved scratchpads yet.</div>";
13108
13373
  return;
13109
13374
  }
13110
- scratchpadRecentPanelEl.innerHTML = "<div class='scratchpad-recent-list'>" + entries.map((entry) => {
13375
+ scratchpadRecentPanelEl.innerHTML = headerHtml + "<div class='scratchpad-recent-list'>" + entries.map((entry) => {
13111
13376
  const key = String(entry && entry.documentKey ? entry.documentKey : "");
13112
13377
  const isCurrent = key === currentKey;
13113
13378
  const label = String(entry && entry.label ? entry.label : key || "scratchpad");
@@ -13145,13 +13410,19 @@
13145
13410
  }
13146
13411
  }
13147
13412
 
13413
+ function hideScratchpadRecentPanel() {
13414
+ scratchpadRecentVisible = false;
13415
+ renderScratchpadRecentPanel();
13416
+ updateScratchpadUi();
13417
+ }
13418
+
13148
13419
  function toggleScratchpadRecentPanel() {
13149
- scratchpadRecentVisible = !scratchpadRecentVisible;
13150
13420
  if (scratchpadRecentVisible) {
13151
- void loadScratchpadRecentEntries();
13152
- } else {
13153
- renderScratchpadRecentPanel();
13421
+ hideScratchpadRecentPanel();
13422
+ return;
13154
13423
  }
13424
+ scratchpadRecentVisible = true;
13425
+ void loadScratchpadRecentEntries();
13155
13426
  updateScratchpadUi();
13156
13427
  }
13157
13428
 
@@ -13181,6 +13452,7 @@
13181
13452
  if (!confirmed) return;
13182
13453
  }
13183
13454
  setScratchpadText(text);
13455
+ hideScratchpadRecentPanel();
13184
13456
  setStatus("Loaded recent scratchpad into current scratchpad.", "success");
13185
13457
  } catch (error) {
13186
13458
  setStatus("Could not use recent scratchpad: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
@@ -16864,7 +17136,7 @@
16864
17136
  const hasNotes = count > 0;
16865
17137
  const isOpen = isReviewNotesOpen();
16866
17138
  if (reviewNotesBtn) {
16867
- reviewNotesBtn.textContent = hasNotes ? "Comments" : "Comments";
17139
+ reviewNotesBtn.textContent = "Comments";
16868
17140
  reviewNotesBtn.classList.toggle("has-content", hasNotes);
16869
17141
  reviewNotesBtn.classList.toggle("is-active", isOpen);
16870
17142
  reviewNotesBtn.setAttribute("aria-pressed", isOpen ? "true" : "false");
@@ -17466,7 +17738,7 @@
17466
17738
  const hasContent = Boolean(normalized.trim());
17467
17739
  const descriptor = getCurrentStudioDocumentDescriptor();
17468
17740
  if (scratchpadBtn) {
17469
- scratchpadBtn.textContent = hasContent ? "Scratchpad" : "Scratchpad";
17741
+ scratchpadBtn.textContent = "Scratchpad";
17470
17742
  scratchpadBtn.classList.toggle("has-content", hasContent);
17471
17743
  scratchpadBtn.title = hasContent
17472
17744
  ? ("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 +17752,9 @@
17480
17752
  if (scratchpadRecentBtn) {
17481
17753
  scratchpadRecentBtn.textContent = scratchpadRecentVisible ? "Hide recent" : "Recent…";
17482
17754
  scratchpadRecentBtn.setAttribute("aria-expanded", scratchpadRecentVisible ? "true" : "false");
17755
+ scratchpadRecentBtn.title = scratchpadRecentVisible
17756
+ ? "Hide recent scratchpads."
17757
+ : "Show recent non-empty scratchpads saved for other files and drafts.";
17483
17758
  }
17484
17759
  if (scratchpadInsertBtn) scratchpadInsertBtn.disabled = !hasContent;
17485
17760
  if (scratchpadCopyBtn) scratchpadCopyBtn.disabled = !hasContent;
@@ -18571,28 +18846,31 @@
18571
18846
  return;
18572
18847
  }
18573
18848
 
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
- );
18849
+ if (message.type === "git_changes_snapshot") {
18850
+ const requestId = typeof message.requestId === "string" ? message.requestId : "";
18851
+ const preserveScroll = Boolean(gitChangesState.requestId && requestId && requestId === gitChangesState.requestId);
18852
+ if (requestId && gitChangesState.requestId && requestId !== gitChangesState.requestId) return;
18853
+ const ok = message.ok !== false;
18854
+ const files = Array.isArray(message.files) ? message.files : [];
18855
+ const selectedPath = files.some((file) => String(file && file.path || "") === String(gitChangesState.selectedPath || ""))
18856
+ ? gitChangesState.selectedPath
18857
+ : (files[0] && files[0].path ? String(files[0].path) : "");
18858
+ gitChangesState = {
18859
+ status: ok ? "ready" : "error",
18860
+ requestId: null,
18861
+ content: ok && typeof message.content === "string" ? message.content : "",
18862
+ label: ok && typeof message.label === "string" ? message.label : "",
18863
+ repoRoot: ok && typeof message.repoRoot === "string" ? message.repoRoot : "",
18864
+ branch: ok && typeof message.branch === "string" ? message.branch : "",
18865
+ hasHead: ok ? message.hasHead !== false : true,
18866
+ files,
18867
+ selectedPath,
18868
+ message: typeof message.message === "string" ? message.message : "",
18869
+ level: typeof message.level === "string" ? message.level : "info",
18870
+ };
18871
+ if (rightView === "changes") renderGitChangesView({ preserveScroll });
18872
+ if (ok) setStatus(files.length ? "Loaded git changes." : "No uncommitted git changes.", files.length ? "success" : "warning");
18873
+ else setStatus(gitChangesState.message || "Could not load git changes.", gitChangesState.level === "error" ? "error" : "warning");
18596
18874
  return;
18597
18875
  }
18598
18876
 
@@ -19248,7 +19526,7 @@
19248
19526
  if (historyPrevBtn) {
19249
19527
  historyPrevBtn.addEventListener("click", () => {
19250
19528
  if (!responseHistory.length) {
19251
- setStatus("No response history available yet.", "warning");
19529
+ setStatus("No branch history available yet.", "warning");
19252
19530
  return;
19253
19531
  }
19254
19532
  selectHistoryIndex(responseHistoryIndex - 1);
@@ -19258,7 +19536,7 @@
19258
19536
  if (historyNextBtn) {
19259
19537
  historyNextBtn.addEventListener("click", () => {
19260
19538
  if (!responseHistory.length) {
19261
- setStatus("No response history available yet.", "warning");
19539
+ setStatus("No branch history available yet.", "warning");
19262
19540
  return;
19263
19541
  }
19264
19542
  selectHistoryIndex(responseHistoryIndex + 1);
@@ -19268,7 +19546,7 @@
19268
19546
  if (historyLastBtn) {
19269
19547
  historyLastBtn.addEventListener("click", () => {
19270
19548
  if (!responseHistory.length) {
19271
- setStatus("No response history available yet.", "warning");
19549
+ setStatus("No branch history available yet.", "warning");
19272
19550
  return;
19273
19551
  }
19274
19552
  selectHistoryIndex(responseHistory.length - 1);
@@ -19295,7 +19573,7 @@
19295
19573
  if (responseHistory.length > 0) {
19296
19574
  selectHistoryIndex(responseHistory.length - 1, { silent: true });
19297
19575
  queuedLatestResponse = null;
19298
- setStatus("Pulled latest response from history.", "success");
19576
+ setStatus("Pulled latest response from branch history.", "success");
19299
19577
  updateResultActionButtons();
19300
19578
  } else if (applyLatestPayload(queuedLatestResponse)) {
19301
19579
  queuedLatestResponse = null;
@@ -19653,27 +19931,6 @@
19653
19931
  });
19654
19932
  }
19655
19933
 
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
19934
  if (zenModeBtn) {
19678
19935
  zenModeBtn.addEventListener("click", () => {
19679
19936
  setStudioZenMode(!studioZenModeEnabled);
@@ -20039,7 +20296,12 @@
20039
20296
  const actionEl = target instanceof Element ? target.closest("[data-scratchpad-recent-action]") : null;
20040
20297
  if (!actionEl) return;
20041
20298
  event.preventDefault();
20299
+ event.stopPropagation();
20042
20300
  const action = String(actionEl.getAttribute("data-scratchpad-recent-action") || "load");
20301
+ if (action === "hide") {
20302
+ hideScratchpadRecentPanel();
20303
+ return;
20304
+ }
20043
20305
  const key = String(actionEl.getAttribute("data-scratchpad-key") || "");
20044
20306
  void applyScratchpadRecentAction(action, key);
20045
20307
  });