@firstpick/pi-package-webui 0.3.6 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/app.js CHANGED
@@ -93,6 +93,13 @@ const elements = {
93
93
  gitPrStatus: $("#gitPrStatus"),
94
94
  gitPrCancelButton: $("#gitPrCancelButton"),
95
95
  gitPrCreateButton: $("#gitPrCreateButton"),
96
+ gitChangesDialog: $("#gitChangesDialog"),
97
+ gitChangesTitle: $("#gitChangesTitle"),
98
+ gitChangesSubtitle: $("#gitChangesSubtitle"),
99
+ gitChangesStatus: $("#gitChangesStatus"),
100
+ gitChangesBody: $("#gitChangesBody"),
101
+ gitChangesRefreshButton: $("#gitChangesRefreshButton"),
102
+ gitChangesCloseButton: $("#gitChangesCloseButton"),
96
103
  modelSelect: $("#modelSelect"),
97
104
  setModelButton: $("#setModelButton"),
98
105
  thinkingSelect: $("#thinkingSelect"),
@@ -219,6 +226,9 @@ let foregroundReconcileTimer = null;
219
226
  let eventSource = null;
220
227
  let activeDialog = null;
221
228
  let activeGitPrDialogResolve = null;
229
+ let gitChangesState = { loading: false, error: "", data: null, tabId: null };
230
+ let gitChangesRequestSerial = 0;
231
+ const gitChangesUntrackedContentRequests = new Set();
222
232
  let nativeCommandTabId = null;
223
233
  let pathPickerState = null;
224
234
  let firstTerminalCwdPromptShown = false;
@@ -305,6 +315,9 @@ let chatUserScrollIntentUntil = 0;
305
315
  let mobileFooterExpanded = false;
306
316
  let footerModelPickerOpen = false;
307
317
  let footerThinkingPickerOpen = false;
318
+ let footerBranchPickerOpen = false;
319
+ let footerBranchPickerState = { loading: false, error: "", branches: [], current: "", root: "", switching: "", tabId: null };
320
+ let footerBranchPickerRequestSerial = 0;
308
321
  let publishMenuOpen = false;
309
322
  let maxVisualViewportHeight = 0;
310
323
  let abortRequestInFlight = false;
@@ -341,6 +354,7 @@ const GIT_FOOTER_WEBUI_STATUS_KEY = "git-footer-webui";
341
354
  const GIT_FOOTER_WEBUI_PAYLOAD_TYPE = "firstpick.git-footer-status.footer";
342
355
  const GIT_FOOTER_WEBUI_PAYLOAD_VERSION = 1;
343
356
  const GIT_FOOTER_WEBUI_PAYLOAD_CACHE_KEY = "pi-webui-git-footer-webui-payload-cache";
357
+ const GIT_CHANGES_RENDER_ROW_LIMIT = 4000;
344
358
  const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
345
359
  const PROMPT_HISTORY_STORAGE_KEY = "pi-webui-prompt-history";
346
360
  const PROMPT_LIST_STORAGE_KEY = "pi-webui-prompt-lists";
@@ -1480,10 +1494,11 @@ function updateComposerModeButtons() {
1480
1494
  }
1481
1495
 
1482
1496
  function isFooterPickerOpen() {
1483
- return footerModelPickerOpen || footerThinkingPickerOpen;
1497
+ return footerModelPickerOpen || footerThinkingPickerOpen || footerBranchPickerOpen;
1484
1498
  }
1485
1499
 
1486
1500
  function footerActivePickerTarget() {
1501
+ if (footerBranchPickerOpen) return elements.statusBar.querySelector(".footer-branch.footer-meta-action");
1487
1502
  if (footerThinkingPickerOpen) return elements.statusBar.querySelector(".footer-thinking.footer-meta-action");
1488
1503
  if (footerModelPickerOpen) return elements.statusBar.querySelector(".footer-model.footer-meta-action, .footer-tui-model");
1489
1504
  return null;
@@ -1534,6 +1549,7 @@ function setMobileFooterExpanded(expanded) {
1534
1549
  if (mobileFooterExpanded && isFooterPickerOpen()) {
1535
1550
  footerModelPickerOpen = false;
1536
1551
  footerThinkingPickerOpen = false;
1552
+ footerBranchPickerOpen = false;
1537
1553
  document.body.classList.remove("footer-model-picker-open");
1538
1554
  elements.statusBar.querySelectorAll(".footer-model-picker").forEach((node) => node.remove());
1539
1555
  }
@@ -1637,6 +1653,7 @@ function updateVisualViewportVars() {
1637
1653
  setMobileTabsExpanded(false);
1638
1654
  setMobileFooterExpanded(false);
1639
1655
  setFooterModelPickerOpen(false);
1656
+ setFooterBranchPickerOpen(false);
1640
1657
  syncMobileChatToBottomForInput();
1641
1658
  }
1642
1659
  updateFooterModelPickerPosition();
@@ -3576,7 +3593,7 @@ function restoreActiveDraft() {
3576
3593
 
3577
3594
  function focusPromptInput({ defer = false } = {}) {
3578
3595
  const focus = () => {
3579
- if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || elements.nativeCommandDialog.open || elements.appRunnerInfoDialog?.open || elements.promptListDialog?.open || elements.attachmentTextDialog?.open || elements.skillEditorDialog?.open || document.visibilityState === "hidden") return;
3596
+ if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || elements.gitChangesDialog?.open || elements.nativeCommandDialog.open || elements.appRunnerInfoDialog?.open || elements.promptListDialog?.open || elements.attachmentTextDialog?.open || elements.skillEditorDialog?.open || document.visibilityState === "hidden") return;
3580
3597
  try {
3581
3598
  elements.promptInput.focus({ preventScroll: true });
3582
3599
  } catch {
@@ -3942,6 +3959,8 @@ async function switchTab(tabId) {
3942
3959
  setMobileTabsExpanded(false);
3943
3960
  footerModelPickerOpen = false;
3944
3961
  footerThinkingPickerOpen = false;
3962
+ footerBranchPickerOpen = false;
3963
+ footerBranchPickerRequestSerial += 1;
3945
3964
  saveActiveDraft();
3946
3965
  const tabContext = setActiveTabId(tabId, { remember: true });
3947
3966
  resetActiveTabUi();
@@ -5048,6 +5067,12 @@ function renderGitFooterPayloadMeta(chip, tab) {
5048
5067
  if (chip.key === "cwd" && tab) {
5049
5068
  options.onClick = changeActiveTabCwd;
5050
5069
  action = `Click to change the working directory for ${tab.title}.`;
5070
+ } else if (chip.key === "git" && chip.value !== "no repo") {
5071
+ options.onClick = () => setFooterBranchPickerOpen(!footerBranchPickerOpen);
5072
+ action = "Click to switch to another local branch.";
5073
+ } else if (chip.key === "changes") {
5074
+ options.onClick = openGitChangesDialog;
5075
+ action = "Click to view the current git diff.";
5051
5076
  } else if (chip.key === "model") {
5052
5077
  options.onClick = () => setFooterModelPickerOpen(!footerModelPickerOpen);
5053
5078
  action = "Click to choose another model.";
@@ -5058,6 +5083,10 @@ function renderGitFooterPayloadMeta(chip, tab) {
5058
5083
  options.title = gitFooterPayloadTooltip(chip, { action });
5059
5084
  options.tooltipAlign = gitFooterTooltipAlign(chip);
5060
5085
  const node = footerMeta(chip.label, chip.value, footerMetaClassForPayload(chip), options);
5086
+ if (chip.key === "git" && options.onClick) {
5087
+ node.setAttribute("aria-haspopup", "listbox");
5088
+ node.setAttribute("aria-expanded", footerBranchPickerOpen ? "true" : "false");
5089
+ }
5061
5090
  return chip.contextUsage ? applyFooterContextUsage(node, chip.contextUsage) : node;
5062
5091
  }
5063
5092
 
@@ -5083,10 +5112,450 @@ function renderGitFooterPayload(payload) {
5083
5112
  elements.statusBar.append(row1, row2);
5084
5113
  if (footerModelPickerOpen) elements.statusBar.append(renderFooterModelPicker());
5085
5114
  if (footerThinkingPickerOpen) elements.statusBar.append(renderFooterThinkingPicker());
5115
+ if (footerBranchPickerOpen) elements.statusBar.append(renderFooterBranchPicker());
5086
5116
  setMobileFooterExpanded(mobileFooterExpanded);
5087
5117
  updateFooterModelPickerPosition();
5088
5118
  }
5089
5119
 
5120
+ function cleanGitDiffPath(value = "") {
5121
+ let text = String(value || "").trim();
5122
+ if (!text || text === "/dev/null") return "";
5123
+ if ((text.startsWith("a/") || text.startsWith("b/")) && text.length > 2) text = text.slice(2);
5124
+ return text;
5125
+ }
5126
+
5127
+ function gitDiffPathFromHeader(line) {
5128
+ const match = String(line || "").match(/^diff --git\s+(.+?)\s+(.+)$/);
5129
+ return cleanGitDiffPath(match?.[2] || match?.[1] || "");
5130
+ }
5131
+
5132
+ function parseGitUnifiedDiff(diffText = "") {
5133
+ const normalized = String(diffText || "").replace(/\r\n?/g, "\n");
5134
+ const lines = normalized.split("\n");
5135
+ const files = [];
5136
+ let file = null;
5137
+ let hunk = null;
5138
+ let oldLineNumber = 0;
5139
+ let newLineNumber = 0;
5140
+ let deleteBuffer = [];
5141
+ let addBuffer = [];
5142
+
5143
+ const flushChangeRows = () => {
5144
+ if (!hunk || (!deleteBuffer.length && !addBuffer.length)) return;
5145
+ if (file) {
5146
+ file.deletions += deleteBuffer.length;
5147
+ file.additions += addBuffer.length;
5148
+ }
5149
+ const rowCount = Math.max(deleteBuffer.length, addBuffer.length);
5150
+ for (let i = 0; i < rowCount; i++) {
5151
+ const left = deleteBuffer[i] || null;
5152
+ const right = addBuffer[i] || null;
5153
+ hunk.rows.push({
5154
+ type: left && right ? "changed" : left ? "removed" : "added",
5155
+ oldNumber: left?.number ?? "",
5156
+ newNumber: right?.number ?? "",
5157
+ left: left?.text ?? "",
5158
+ right: right?.text ?? "",
5159
+ });
5160
+ }
5161
+ deleteBuffer = [];
5162
+ addBuffer = [];
5163
+ };
5164
+
5165
+ const finishFile = () => {
5166
+ flushChangeRows();
5167
+ if (!file) return;
5168
+ file.path = file.newPath || file.oldPath || file.headerPath || "diff";
5169
+ files.push(file);
5170
+ file = null;
5171
+ hunk = null;
5172
+ };
5173
+
5174
+ for (let index = 0; index < lines.length; index++) {
5175
+ const line = lines[index] || "";
5176
+ if (index === lines.length - 1 && !line && normalized.endsWith("\n")) continue;
5177
+
5178
+ if (line.startsWith("diff --git ")) {
5179
+ finishFile();
5180
+ file = { path: "", oldPath: "", newPath: "", headerPath: gitDiffPathFromHeader(line), headers: [line], hunks: [], additions: 0, deletions: 0 };
5181
+ continue;
5182
+ }
5183
+
5184
+ if (!file) {
5185
+ if (!line.trim()) continue;
5186
+ file = { path: "diff", oldPath: "", newPath: "", headerPath: "diff", headers: [], hunks: [], additions: 0, deletions: 0 };
5187
+ }
5188
+
5189
+ if (line.startsWith("@@ ")) {
5190
+ flushChangeRows();
5191
+ const match = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
5192
+ oldLineNumber = Number.parseInt(match?.[1] || "0", 10) || 0;
5193
+ newLineNumber = Number.parseInt(match?.[2] || "0", 10) || 0;
5194
+ hunk = { header: line, rows: [] };
5195
+ file.hunks.push(hunk);
5196
+ continue;
5197
+ }
5198
+
5199
+ if (!hunk) {
5200
+ file.headers.push(line);
5201
+ if (line.startsWith("--- ")) file.oldPath = cleanGitDiffPath(line.slice(4));
5202
+ if (line.startsWith("+++ ")) file.newPath = cleanGitDiffPath(line.slice(4));
5203
+ continue;
5204
+ }
5205
+
5206
+ if (line.startsWith("-")) {
5207
+ deleteBuffer.push({ number: oldLineNumber, text: line.slice(1) });
5208
+ oldLineNumber += 1;
5209
+ continue;
5210
+ }
5211
+ if (line.startsWith("+")) {
5212
+ addBuffer.push({ number: newLineNumber, text: line.slice(1) });
5213
+ newLineNumber += 1;
5214
+ continue;
5215
+ }
5216
+
5217
+ flushChangeRows();
5218
+ if (line.startsWith(" ")) {
5219
+ const text = line.slice(1);
5220
+ hunk.rows.push({ type: "context", oldNumber: oldLineNumber, newNumber: newLineNumber, left: text, right: text });
5221
+ oldLineNumber += 1;
5222
+ newLineNumber += 1;
5223
+ } else if (line.startsWith("\\")) {
5224
+ hunk.rows.push({ type: "meta", oldNumber: "", newNumber: "", left: line, right: line });
5225
+ } else {
5226
+ hunk.rows.push({ type: "meta", oldNumber: "", newNumber: "", left: line, right: line });
5227
+ }
5228
+ }
5229
+
5230
+ finishFile();
5231
+ return files;
5232
+ }
5233
+
5234
+ function gitChangesChip(label, value, className = "") {
5235
+ const chip = make("div", `git-changes-chip ${className}`.trim());
5236
+ chip.append(make("span", "git-changes-chip-label", label), make("span", "git-changes-chip-value", String(value ?? "—")));
5237
+ return chip;
5238
+ }
5239
+
5240
+ function renderGitChangesOverview(data) {
5241
+ const summary = data?.summary || {};
5242
+ const untrackedCount = Array.isArray(data?.untracked) ? data.untracked.length : Number(summary.untracked || 0);
5243
+ const overview = make("div", "git-changes-overview");
5244
+ overview.append(
5245
+ gitChangesChip("repo", data?.root || "—", "wide"),
5246
+ gitChangesChip("branch", data?.branch || "detached"),
5247
+ gitChangesChip("staged", summary.staged || 0, "success"),
5248
+ gitChangesChip("modified", summary.unstaged || 0, "warning"),
5249
+ gitChangesChip("untracked", untrackedCount, "muted"),
5250
+ gitChangesChip("conflicts", summary.conflicted || 0, (summary.conflicted || 0) > 0 ? "danger" : "muted"),
5251
+ );
5252
+ return overview;
5253
+ }
5254
+
5255
+ function renderGitDiffRow(row) {
5256
+ const node = make("div", `git-diff-row ${row.type || "context"}`.trim());
5257
+ node.append(
5258
+ make("span", "git-diff-line-number old", row.oldNumber === "" ? "" : String(row.oldNumber)),
5259
+ make("code", "git-diff-line old", row.left ?? ""),
5260
+ make("span", "git-diff-line-number new", row.newNumber === "" ? "" : String(row.newNumber)),
5261
+ make("code", "git-diff-line new", row.right ?? ""),
5262
+ );
5263
+ return node;
5264
+ }
5265
+
5266
+ function renderGitDiffGrid(file) {
5267
+ const grid = make("div", "git-diff-grid");
5268
+ const rowLimit = file.renderRowLimit ?? GIT_CHANGES_RENDER_ROW_LIMIT;
5269
+ let renderedRows = 0;
5270
+ let truncated = false;
5271
+ for (const hunk of file.hunks || []) {
5272
+ if (renderedRows >= rowLimit) {
5273
+ truncated = true;
5274
+ break;
5275
+ }
5276
+ grid.append(renderGitDiffRow({ type: "hunk", oldNumber: "", newNumber: "", left: hunk.header, right: hunk.header }));
5277
+ renderedRows += 1;
5278
+ for (const row of hunk.rows || []) {
5279
+ if (renderedRows >= rowLimit) {
5280
+ truncated = true;
5281
+ break;
5282
+ }
5283
+ grid.append(renderGitDiffRow(row));
5284
+ renderedRows += 1;
5285
+ }
5286
+ if (truncated) break;
5287
+ }
5288
+ if (truncated) {
5289
+ grid.append(renderGitDiffRow({ type: "meta", oldNumber: "", newNumber: "", left: `Diff preview truncated after ${rowLimit} rows.`, right: "Use git diff in the terminal for the full output." }));
5290
+ }
5291
+ return grid;
5292
+ }
5293
+
5294
+ function renderGitDiffFile(file) {
5295
+ const details = make("details", `git-diff-file ${file.className || ""}`.trim());
5296
+ details.open = true;
5297
+ details.dataset.gitDiffFile = file.path || "diff";
5298
+ const summary = make("summary", "git-diff-file-summary");
5299
+ summary.append(
5300
+ make("span", "git-diff-file-name", file.path || "diff"),
5301
+ make("span", "git-diff-file-stats", file.statsText || `+${file.additions || 0} −${file.deletions || 0}`),
5302
+ );
5303
+ details.append(summary);
5304
+ if (file.hunks?.length) {
5305
+ details.append(renderGitDiffGrid(file));
5306
+ } else {
5307
+ details.append(make("pre", "git-diff-raw", (file.headers || []).join("\n") || "No textual diff for this file."));
5308
+ }
5309
+ return details;
5310
+ }
5311
+
5312
+ function renderGitDiffSection(section, files) {
5313
+ const key = String(section?.key || "diff").replace(/[^a-z0-9_-]/gi, "-");
5314
+ const wrapper = make("section", `git-diff-section git-diff-section-${key}`);
5315
+ const header = make("div", "git-diff-section-heading");
5316
+ header.append(
5317
+ make("div", "git-diff-section-title", section?.label || "Git diff"),
5318
+ make("div", "git-diff-section-meta", `${files.length} file${files.length === 1 ? "" : "s"} · ${section?.command || "git diff"}`),
5319
+ );
5320
+ wrapper.append(header, ...files.map(renderGitDiffFile));
5321
+ return wrapper;
5322
+ }
5323
+
5324
+ function normalizeGitUntrackedEntry(value) {
5325
+ if (typeof value === "string") return { path: value, size: 0, binary: false, content: "", contentMissing: true };
5326
+ if (!value || typeof value !== "object") return null;
5327
+ const path = String(value.path || "").trim();
5328
+ if (!path) return null;
5329
+ const hasContent = Object.prototype.hasOwnProperty.call(value, "content");
5330
+ const binary = value.binary === true;
5331
+ const error = value.error ? String(value.error) : "";
5332
+ return {
5333
+ path,
5334
+ size: Number(value.size || 0) || 0,
5335
+ binary,
5336
+ content: hasContent && typeof value.content === "string" ? value.content : "",
5337
+ contentMissing: !hasContent && !binary && !error,
5338
+ error,
5339
+ };
5340
+ }
5341
+
5342
+ function gitUntrackedEntries(untracked) {
5343
+ return Array.isArray(untracked) ? untracked.map(normalizeGitUntrackedEntry).filter(Boolean) : [];
5344
+ }
5345
+
5346
+ function gitUntrackedContentLines(content = "") {
5347
+ const normalized = String(content || "").replace(/\r\n?/g, "\n");
5348
+ if (!normalized) return [];
5349
+ const withoutFinalNewline = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
5350
+ return withoutFinalNewline ? withoutFinalNewline.split("\n") : [""];
5351
+ }
5352
+
5353
+ function gitUntrackedEntryToDiffFile(entry) {
5354
+ const lines = gitUntrackedContentLines(entry.content);
5355
+ return {
5356
+ path: entry.path,
5357
+ className: "git-untracked-full-file",
5358
+ additions: lines.length,
5359
+ deletions: 0,
5360
+ statsText: `${entry.binary ? "binary" : `+${lines.length}`} · ${formatBytes(entry.size)}`,
5361
+ renderRowLimit: Number.POSITIVE_INFINITY,
5362
+ headers: lines.length ? [] : ["Empty untracked file."],
5363
+ hunks: lines.length ? [{
5364
+ header: `@@ -0,0 +1,${lines.length} @@`,
5365
+ rows: lines.map((line, index) => ({ type: "added", oldNumber: "", newNumber: index + 1, left: "", right: line })),
5366
+ }] : [],
5367
+ };
5368
+ }
5369
+
5370
+ function renderGitUntrackedRawFile(entry) {
5371
+ const details = make("details", "git-diff-file git-untracked-full-file");
5372
+ details.open = true;
5373
+ details.dataset.gitDiffFile = entry.path;
5374
+ const summary = make("summary", "git-diff-file-summary");
5375
+ summary.append(
5376
+ make("span", "git-diff-file-name", entry.path),
5377
+ make("span", "git-diff-file-stats", entry.error ? "unreadable" : `binary · ${formatBytes(entry.size)}`),
5378
+ );
5379
+ details.append(summary, make("pre", "git-diff-raw", entry.error || "Binary untracked file; text preview unavailable."));
5380
+ return details;
5381
+ }
5382
+
5383
+ function renderGitUntrackedLoadingFile(entry) {
5384
+ const details = make("details", "git-diff-file git-untracked-full-file git-untracked-loading-file");
5385
+ details.open = true;
5386
+ details.dataset.gitDiffFile = entry.path;
5387
+ const summary = make("summary", "git-diff-file-summary");
5388
+ summary.append(make("span", "git-diff-file-name", entry.path), make("span", "git-diff-file-stats", "loading content"));
5389
+ details.append(summary, make("pre", "git-diff-raw", "Loading complete untracked file content…"));
5390
+ return details;
5391
+ }
5392
+
5393
+ function renderGitUntrackedFile(entry) {
5394
+ if (entry.contentMissing) return renderGitUntrackedLoadingFile(entry);
5395
+ if (entry.error || entry.binary) return renderGitUntrackedRawFile(entry);
5396
+ return renderGitDiffFile(gitUntrackedEntryToDiffFile(entry));
5397
+ }
5398
+
5399
+ function replaceGitUntrackedEntry(entry, tabId = gitChangesState.tabId) {
5400
+ const data = gitChangesState.data;
5401
+ if (!data || tabId !== gitChangesState.tabId) return;
5402
+ const entries = gitUntrackedEntries(data.untracked);
5403
+ const nextEntries = entries.map((item) => item.path === entry.path ? normalizeGitUntrackedEntry(entry) : item);
5404
+ gitChangesState = { ...gitChangesState, data: { ...data, untracked: nextEntries } };
5405
+ renderGitChangesDialog();
5406
+ }
5407
+
5408
+ async function loadMissingGitUntrackedContent(entry, tabId = gitChangesState.tabId) {
5409
+ const key = `${tabId || ""}\u0000${entry.path}`;
5410
+ if (!entry.contentMissing || gitChangesUntrackedContentRequests.has(key)) return;
5411
+ gitChangesUntrackedContentRequests.add(key);
5412
+ try {
5413
+ const response = await api(`/api/git-changes/untracked-file?path=${encodeURIComponent(entry.path)}`, { tabId });
5414
+ if (!response.ok) throw new Error(response.error || "Failed to load untracked file content");
5415
+ replaceGitUntrackedEntry(response.data, tabId);
5416
+ } catch (error) {
5417
+ replaceGitUntrackedEntry({ ...entry, contentMissing: false, error: error.message || String(error) }, tabId);
5418
+ } finally {
5419
+ gitChangesUntrackedContentRequests.delete(key);
5420
+ }
5421
+ }
5422
+
5423
+ function renderGitUntrackedSection(untracked) {
5424
+ const entries = gitUntrackedEntries(untracked);
5425
+ const wrapper = make("section", "git-diff-section git-diff-section-untracked");
5426
+ const header = make("div", "git-diff-section-heading");
5427
+ header.append(
5428
+ make("div", "git-diff-section-title", "Untracked"),
5429
+ make("div", "git-diff-section-meta", `${entries.length} file${entries.length === 1 ? "" : "s"} · complete file contents`),
5430
+ );
5431
+ wrapper.append(header, ...entries.map(renderGitUntrackedFile));
5432
+ for (const entry of entries) {
5433
+ if (entry.contentMissing) queueMicrotask(() => loadMissingGitUntrackedContent(entry));
5434
+ }
5435
+ return wrapper;
5436
+ }
5437
+
5438
+ function renderGitCurrentFileHeader() {
5439
+ const header = make("div", "git-current-file-header");
5440
+ header.append(make("span", "git-current-file-label", "Current file"), make("span", "git-current-file-name", "—"));
5441
+ return header;
5442
+ }
5443
+
5444
+ function updateGitChangesCurrentFileHeader() {
5445
+ const body = elements.gitChangesBody;
5446
+ const header = body?.querySelector(".git-current-file-header");
5447
+ const name = header?.querySelector(".git-current-file-name");
5448
+ if (!body || !header || !name) return;
5449
+ const files = Array.from(body.querySelectorAll(".git-diff-file[data-git-diff-file]"));
5450
+ if (!files.length) {
5451
+ name.textContent = "—";
5452
+ return;
5453
+ }
5454
+ const bodyRect = body.getBoundingClientRect();
5455
+ const headerRect = header.getBoundingClientRect();
5456
+ const markerY = Math.min(bodyRect.bottom - 1, Math.max(bodyRect.top, headerRect.bottom + 8));
5457
+ let current = files[0];
5458
+ for (const file of files) {
5459
+ const rect = file.getBoundingClientRect();
5460
+ if (rect.top <= markerY && rect.bottom > markerY) {
5461
+ current = file;
5462
+ break;
5463
+ }
5464
+ if (rect.top <= markerY) current = file;
5465
+ else break;
5466
+ }
5467
+ name.textContent = current?.dataset.gitDiffFile || "—";
5468
+ }
5469
+
5470
+ function gitChangesGeneratedLabel(data) {
5471
+ const timestamp = Date.parse(data?.generatedAt || "");
5472
+ if (!Number.isFinite(timestamp)) return "";
5473
+ return `Updated ${new Date(timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" })}`;
5474
+ }
5475
+
5476
+ function renderGitChangesDialog() {
5477
+ if (!elements.gitChangesDialog || !elements.gitChangesBody) return;
5478
+ const { loading, error, data } = gitChangesState;
5479
+ if (elements.gitChangesTitle) elements.gitChangesTitle.textContent = "Uncommitted Changes";
5480
+ if (elements.gitChangesSubtitle) elements.gitChangesSubtitle.textContent = data?.root ? `${data.branch || "detached"} · ${data.root}` : "Current tab git diff";
5481
+ if (elements.gitChangesRefreshButton) {
5482
+ elements.gitChangesRefreshButton.disabled = loading;
5483
+ elements.gitChangesRefreshButton.textContent = loading ? "Refreshing…" : "Refresh";
5484
+ }
5485
+ if (elements.gitChangesStatus) {
5486
+ elements.gitChangesStatus.className = `git-changes-status ${error ? "error" : "muted"}`;
5487
+ elements.gitChangesStatus.textContent = error || (loading ? "Loading git diff…" : data ? gitChangesGeneratedLabel(data) : "");
5488
+ elements.gitChangesStatus.hidden = !elements.gitChangesStatus.textContent;
5489
+ }
5490
+
5491
+ const body = elements.gitChangesBody;
5492
+ body.replaceChildren();
5493
+ if (loading && !data) {
5494
+ body.append(make("div", "git-changes-empty", "Loading git diff…"));
5495
+ return;
5496
+ }
5497
+ if (error) {
5498
+ body.append(make("div", "git-changes-empty error", error));
5499
+ return;
5500
+ }
5501
+ if (!data) {
5502
+ body.append(make("div", "git-changes-empty", "Open from the footer CHANGES chip to load the current git diff."));
5503
+ return;
5504
+ }
5505
+
5506
+ body.append(renderGitChangesOverview(data));
5507
+ const parsedSections = (Array.isArray(data.sections) ? data.sections : [])
5508
+ .map((section) => ({ section, files: parseGitUnifiedDiff(section.diff || "") }))
5509
+ .filter((entry) => entry.files.length > 0);
5510
+ const untracked = gitUntrackedEntries(data.untracked);
5511
+ const hasVisibleFiles = parsedSections.length > 0 || untracked.length > 0;
5512
+ if (hasVisibleFiles) body.append(renderGitCurrentFileHeader());
5513
+ for (const entry of parsedSections) body.append(renderGitDiffSection(entry.section, entry.files));
5514
+ if (untracked.length) body.append(renderGitUntrackedSection(untracked));
5515
+ if (!hasVisibleFiles) body.append(make("div", "git-changes-empty success", "Working tree is clean. No staged or unstaged diff."));
5516
+ if (hasVisibleFiles) requestAnimationFrame(updateGitChangesCurrentFileHeader);
5517
+ }
5518
+
5519
+ async function loadGitChangesDialog(tabContext = activeTabContext()) {
5520
+ const requestSerial = ++gitChangesRequestSerial;
5521
+ gitChangesUntrackedContentRequests.clear();
5522
+ gitChangesState = { ...gitChangesState, loading: true, error: "", tabId: tabContext.tabId || activeTabId };
5523
+ renderGitChangesDialog();
5524
+ try {
5525
+ const response = await api("/api/git-changes", { tabId: tabContext.tabId });
5526
+ if (requestSerial !== gitChangesRequestSerial) return;
5527
+ if (!response.ok) throw new Error(response.error || "Failed to load git changes");
5528
+ gitChangesState = { loading: false, error: "", data: response.data || null, tabId: tabContext.tabId || activeTabId };
5529
+ } catch (error) {
5530
+ if (requestSerial !== gitChangesRequestSerial) return;
5531
+ gitChangesState = { ...gitChangesState, loading: false, error: error.message || String(error) };
5532
+ }
5533
+ renderGitChangesDialog();
5534
+ }
5535
+
5536
+ function openGitChangesDialog() {
5537
+ if (!elements.gitChangesDialog) return;
5538
+ hideFooterTooltip();
5539
+ const tabContext = activeTabContext();
5540
+ const tabId = tabContext.tabId || activeTabId;
5541
+ gitChangesState = { loading: true, error: "", data: gitChangesState.tabId === tabId ? gitChangesState.data : null, tabId };
5542
+ renderGitChangesDialog();
5543
+ if (!elements.gitChangesDialog.open) elements.gitChangesDialog.showModal();
5544
+ loadGitChangesDialog(tabContext).catch((error) => addEvent(error.message || String(error), "error"));
5545
+ }
5546
+
5547
+ function refreshGitChangesDialog() {
5548
+ const tabContext = { tabId: gitChangesState.tabId || activeTabId };
5549
+ loadGitChangesDialog(tabContext).catch((error) => addEvent(error.message || String(error), "error"));
5550
+ }
5551
+
5552
+ function closeGitChangesDialog() {
5553
+ gitChangesRequestSerial += 1;
5554
+ gitChangesUntrackedContentRequests.clear();
5555
+ gitChangesState = { ...gitChangesState, loading: false };
5556
+ if (elements.gitChangesDialog?.open) elements.gitChangesDialog.close();
5557
+ }
5558
+
5090
5559
  function gitFooterFallbackMessage() {
5091
5560
  if (isOptionalFeatureDisabled("gitFooterStatus")) return "";
5092
5561
  const tabContext = activeTabContext();
@@ -5118,13 +5587,18 @@ function renderMinimalFooter() {
5118
5587
  }));
5119
5588
  if (footerModelPickerOpen) elements.statusBar.append(renderFooterModelPicker());
5120
5589
  if (footerThinkingPickerOpen) elements.statusBar.append(renderFooterThinkingPicker());
5590
+ if (footerBranchPickerOpen) elements.statusBar.append(renderFooterBranchPicker());
5121
5591
  setMobileFooterExpanded(false);
5122
5592
  updateFooterModelPickerPosition();
5123
5593
  }
5124
5594
 
5125
5595
  function setFooterModelPickerOpen(open) {
5126
5596
  footerModelPickerOpen = !!open;
5127
- if (footerModelPickerOpen) footerThinkingPickerOpen = false;
5597
+ if (footerModelPickerOpen) {
5598
+ footerThinkingPickerOpen = false;
5599
+ footerBranchPickerOpen = false;
5600
+ footerBranchPickerRequestSerial += 1;
5601
+ }
5128
5602
  if (footerModelPickerOpen && isMobileView()) {
5129
5603
  mobileFooterExpanded = false;
5130
5604
  document.body.classList.remove("footer-details-expanded");
@@ -5138,7 +5612,11 @@ function setFooterModelPickerOpen(open) {
5138
5612
 
5139
5613
  function setFooterThinkingPickerOpen(open) {
5140
5614
  footerThinkingPickerOpen = !!open;
5141
- if (footerThinkingPickerOpen) footerModelPickerOpen = false;
5615
+ if (footerThinkingPickerOpen) {
5616
+ footerModelPickerOpen = false;
5617
+ footerBranchPickerOpen = false;
5618
+ footerBranchPickerRequestSerial += 1;
5619
+ }
5142
5620
  if (footerThinkingPickerOpen && isMobileView()) {
5143
5621
  mobileFooterExpanded = false;
5144
5622
  document.body.classList.remove("footer-details-expanded");
@@ -5150,6 +5628,239 @@ function setFooterThinkingPickerOpen(open) {
5150
5628
  updateFooterModelPickerPosition();
5151
5629
  }
5152
5630
 
5631
+ function normalizeFooterGitBranches(data = {}) {
5632
+ const current = cleanStatusText(data.current || "");
5633
+ const seen = new Set();
5634
+ const branches = [];
5635
+ for (const item of Array.isArray(data.branches) ? data.branches : []) {
5636
+ const name = cleanStatusText(typeof item === "string" ? item : item?.name);
5637
+ if (!name || seen.has(name)) continue;
5638
+ seen.add(name);
5639
+ branches.push({ name, current: Boolean(item?.current) || (!!current && name === current) });
5640
+ }
5641
+ return {
5642
+ root: cleanFooterPayloadText(data.root, "", 4000),
5643
+ current,
5644
+ branches,
5645
+ };
5646
+ }
5647
+
5648
+ function applyOptimisticGitFooterBranch(branch, tabContext = activeTabContext()) {
5649
+ const nextBranch = cleanStatusText(branch);
5650
+ if (!nextBranch) return;
5651
+ const raw = statusEntries.get(GIT_FOOTER_WEBUI_STATUS_KEY) || readCachedGitFooterWebuiPayloadRaw();
5652
+ const payload = parseGitFooterWebuiPayloadRaw(raw);
5653
+ if (!payload) return;
5654
+ const nextPayload = {
5655
+ type: GIT_FOOTER_WEBUI_PAYLOAD_TYPE,
5656
+ version: GIT_FOOTER_WEBUI_PAYLOAD_VERSION,
5657
+ generatedAt: Date.now(),
5658
+ main: payload.main,
5659
+ meta: payload.meta.map((chip) => chip.key === "git" ? { ...chip, value: nextBranch, title: `git branch: ${nextBranch}` } : chip),
5660
+ };
5661
+ const nextRaw = JSON.stringify(nextPayload);
5662
+ statusEntries.set(GIT_FOOTER_WEBUI_STATUS_KEY, nextRaw);
5663
+ cacheGitFooterWebuiPayload(nextRaw, tabContext.tabId);
5664
+ }
5665
+
5666
+ async function loadFooterBranchPicker(tabContext = activeTabContext()) {
5667
+ const requestSerial = ++footerBranchPickerRequestSerial;
5668
+ const tabId = tabContext.tabId || activeTabId;
5669
+ footerBranchPickerState = {
5670
+ loading: true,
5671
+ error: "",
5672
+ branches: footerBranchPickerState.tabId === tabId ? footerBranchPickerState.branches : [],
5673
+ current: footerBranchPickerState.tabId === tabId ? footerBranchPickerState.current : "",
5674
+ root: footerBranchPickerState.tabId === tabId ? footerBranchPickerState.root : "",
5675
+ switching: "",
5676
+ tabId,
5677
+ };
5678
+ if (isCurrentTabContext(tabContext)) {
5679
+ renderFooter();
5680
+ updateFooterModelPickerPosition();
5681
+ }
5682
+ try {
5683
+ const response = await api("/api/git-branches", { tabId });
5684
+ if (requestSerial !== footerBranchPickerRequestSerial || !footerBranchPickerOpen || !isCurrentTabContext(tabContext)) return;
5685
+ if (!response.ok) throw new Error(response.error || "Failed to load git branches");
5686
+ footerBranchPickerState = { loading: false, error: "", switching: "", tabId, ...normalizeFooterGitBranches(response.data || {}) };
5687
+ } catch (error) {
5688
+ if (requestSerial !== footerBranchPickerRequestSerial || !footerBranchPickerOpen || !isCurrentTabContext(tabContext)) return;
5689
+ footerBranchPickerState = { ...footerBranchPickerState, loading: false, switching: "", error: error.message || String(error) };
5690
+ }
5691
+ if (isCurrentTabContext(tabContext)) {
5692
+ renderFooter();
5693
+ updateFooterModelPickerPosition();
5694
+ }
5695
+ }
5696
+
5697
+ function setFooterBranchPickerOpen(open) {
5698
+ footerBranchPickerOpen = !!open;
5699
+ if (footerBranchPickerOpen) {
5700
+ footerModelPickerOpen = false;
5701
+ footerThinkingPickerOpen = false;
5702
+ if (isMobileView()) {
5703
+ mobileFooterExpanded = false;
5704
+ document.body.classList.remove("footer-details-expanded");
5705
+ setComposerActionsOpen(false);
5706
+ setMobileTabsExpanded(false);
5707
+ }
5708
+ loadFooterBranchPicker(activeTabContext()).catch((error) => addEvent(error.message || String(error), "error"));
5709
+ } else {
5710
+ footerBranchPickerRequestSerial += 1;
5711
+ }
5712
+ document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
5713
+ renderFooter();
5714
+ updateFooterModelPickerPosition();
5715
+ }
5716
+
5717
+ function pathLooksInside(parentPath, childPath) {
5718
+ const normalizePath = (value) => String(value || "").replace(/\\+/g, "/").replace(/\/+$/, "");
5719
+ const parent = normalizePath(parentPath);
5720
+ const child = normalizePath(childPath);
5721
+ return !!parent && !!child && (child === parent || child.startsWith(`${parent}/`));
5722
+ }
5723
+
5724
+ function footerBranchActiveAgentTabs(tabContext = activeTabContext()) {
5725
+ const active = activeTab();
5726
+ const activeCwd = latestWorkspace?.cwd || active?.cwd || "";
5727
+ const root = footerBranchPickerState.root || "";
5728
+ return tabs.filter((tab) => {
5729
+ const sameWorktree = root ? pathLooksInside(root, tab.cwd) : !!activeCwd && tab.cwd === activeCwd;
5730
+ if (!sameWorktree) return false;
5731
+ if (tab.id === tabContext.tabId) return currentState?.isStreaming || currentState?.isCompacting || tabHasActiveAgent(tab);
5732
+ return tabHasActiveAgent(tab);
5733
+ });
5734
+ }
5735
+
5736
+ function footerBranchAgentWarningLines(tabContext = activeTabContext()) {
5737
+ const busyTabs = footerBranchActiveAgentTabs(tabContext);
5738
+ if (!busyTabs.length) return [];
5739
+ const list = busyTabs.slice(0, 4).map((tab) => `- ${tab.title || tab.id}`).join("\n");
5740
+ const extra = busyTabs.length > 4 ? `\n- … +${busyTabs.length - 4} more` : "";
5741
+ return [
5742
+ "",
5743
+ `WARNING: ${busyTabs.length === 1 ? "An agent is" : "Agents are"} still running or waiting for input in this Git working tree:`,
5744
+ `${list}${extra}`,
5745
+ "Switching branches can change files underneath the running agent.",
5746
+ ];
5747
+ }
5748
+
5749
+ function confirmFooterGitBranchAction(branch, { create = false, requireConfirm = false, tabContext = activeTabContext() } = {}) {
5750
+ const branchName = cleanStatusText(branch);
5751
+ const warningLines = footerBranchAgentWarningLines(tabContext);
5752
+ if (!requireConfirm && warningLines.length === 0) return true;
5753
+ const action = create ? "Create and switch to new git branch" : "Switch git branch";
5754
+ const message = [
5755
+ `${action}: ${branchName}?`,
5756
+ "",
5757
+ `Repository: ${footerBranchPickerState.root || currentGitFooterCacheCwd(tabContext.tabId) || "current tab"}`,
5758
+ ...warningLines,
5759
+ "",
5760
+ "Continue?",
5761
+ ].join("\n");
5762
+ return window.confirm(message);
5763
+ }
5764
+
5765
+ function promptFooterGitBranchName() {
5766
+ const value = window.prompt("New git branch name:", "");
5767
+ if (value === null) return "";
5768
+ return cleanStatusText(value);
5769
+ }
5770
+
5771
+ async function createFooterGitBranch() {
5772
+ const branchName = promptFooterGitBranchName();
5773
+ if (!branchName) return;
5774
+ const tabContext = activeTabContext();
5775
+ if (!confirmFooterGitBranchAction(branchName, { create: true, requireConfirm: true, tabContext })) return;
5776
+ await applyFooterGitBranch(branchName, { create: true, tabContext, skipConfirm: true });
5777
+ }
5778
+
5779
+ async function applyFooterGitBranch(branch, { create = false, tabContext = activeTabContext(), skipConfirm = false } = {}) {
5780
+ const branchName = cleanStatusText(branch);
5781
+ if (!branchName) return;
5782
+ const tabId = tabContext.tabId || activeTabId;
5783
+ if (!skipConfirm && !confirmFooterGitBranchAction(branchName, { create, tabContext })) return;
5784
+ try {
5785
+ footerBranchPickerState = { ...footerBranchPickerState, loading: true, error: "", switching: branchName, tabId };
5786
+ renderFooter();
5787
+ const response = await api("/api/git-branch", { method: "POST", body: { branch: branchName, create }, tabId });
5788
+ if (!isCurrentTabContext(tabContext)) return;
5789
+ if (!response.ok) throw new Error(response.error || `Failed to ${create ? "create and switch to" : "switch to"} ${branchName}`);
5790
+ const switchedBranch = cleanStatusText(response.data?.branch || branchName);
5791
+ footerBranchPickerOpen = false;
5792
+ footerBranchPickerRequestSerial += 1;
5793
+ footerBranchPickerState = { ...footerBranchPickerState, loading: false, switching: "", current: switchedBranch };
5794
+ applyOptimisticGitFooterBranch(switchedBranch, tabContext);
5795
+ addEvent(response.data?.created ? `Created and switched to git branch ${switchedBranch}.` : response.data?.switched === false ? `Already on git branch ${switchedBranch}.` : `Switched git branch to ${switchedBranch}.`, "info");
5796
+ requestGitFooterWebuiPayload(tabContext, { force: true });
5797
+ } catch (error) {
5798
+ if (isCurrentTabContext(tabContext)) {
5799
+ footerBranchPickerState = { ...footerBranchPickerState, loading: false, switching: "", error: error.message || String(error) };
5800
+ addEvent(error.message || String(error), "error");
5801
+ }
5802
+ } finally {
5803
+ if (isCurrentTabContext(tabContext)) {
5804
+ document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
5805
+ renderFooter();
5806
+ updateFooterModelPickerPosition();
5807
+ }
5808
+ }
5809
+ }
5810
+
5811
+ function renderFooterBranchPicker() {
5812
+ const picker = make("div", "footer-model-picker footer-branch-picker");
5813
+ picker.setAttribute("role", "listbox");
5814
+ picker.setAttribute("aria-label", "Git branches");
5815
+ const state = footerBranchPickerState;
5816
+ const current = state.current || "detached";
5817
+ picker.append(make("div", "footer-model-picker-title", "Git branches"));
5818
+ picker.append(make("div", "footer-model-picker-source", `${state.loading ? "Refreshing" : "Current"}: ${state.switching || current}${state.root ? ` · ${state.root}` : ""}`));
5819
+
5820
+ if (state.error) {
5821
+ const error = make("div", "footer-model-picker-empty error");
5822
+ error.append(make("strong", undefined, "Cannot load branches."), make("span", undefined, ` ${state.error}`));
5823
+ picker.append(error);
5824
+ return picker;
5825
+ }
5826
+ if (state.loading && state.branches.length === 0) {
5827
+ picker.append(make("div", "footer-model-picker-empty muted", "Loading local branches…"));
5828
+ return picker;
5829
+ }
5830
+
5831
+ const hasOtherBranches = state.branches.some((branch) => !branch.current && branch.name !== state.current);
5832
+ if (!state.loading && !hasOtherBranches) {
5833
+ const empty = make("div", "footer-model-picker-empty muted");
5834
+ empty.append(make("strong", undefined, "No other local branches available."), make("span", undefined, " Create a branch from the current HEAD to continue."));
5835
+ const createButton = make("button", "footer-model-option footer-branch-create-option");
5836
+ createButton.type = "button";
5837
+ createButton.append(
5838
+ make("span", "footer-model-option-main", "Create new branch"),
5839
+ make("span", "footer-model-option-name", "prompts for a name, confirms, then runs git switch -c"),
5840
+ );
5841
+ createButton.addEventListener("click", () => createFooterGitBranch().catch((error) => addEvent(error.message || String(error), "error")));
5842
+ picker.append(empty, createButton);
5843
+ }
5844
+
5845
+ for (const branch of state.branches) {
5846
+ const selected = branch.current || (!!state.current && branch.name === state.current);
5847
+ const disabled = selected || state.loading || !!state.switching;
5848
+ const button = make("button", `footer-model-option footer-branch-option${selected ? " active" : ""}`);
5849
+ button.type = "button";
5850
+ button.disabled = disabled;
5851
+ button.setAttribute("role", "option");
5852
+ button.setAttribute("aria-selected", selected ? "true" : "false");
5853
+ button.title = selected ? `Current branch: ${branch.name}` : `git switch ${branch.name}`;
5854
+ button.append(
5855
+ make("span", "footer-model-option-main", branch.name),
5856
+ make("span", "footer-model-option-name", selected ? "current branch" : state.switching === branch.name ? "switching…" : "switch to this branch"),
5857
+ );
5858
+ if (!disabled) button.addEventListener("click", () => applyFooterGitBranch(branch.name));
5859
+ picker.append(button);
5860
+ }
5861
+ return picker;
5862
+ }
5863
+
5153
5864
  async function applyFooterModel(model) {
5154
5865
  if (!model?.provider || !model?.id) return;
5155
5866
  const tabContext = activeTabContext();
@@ -13661,6 +14372,7 @@ document.addEventListener("pointerdown", (event) => {
13661
14372
  if (isFooterPickerOpen() && !elements.statusBar.contains(event.target)) {
13662
14373
  setFooterModelPickerOpen(false);
13663
14374
  setFooterThinkingPickerOpen(false);
14375
+ setFooterBranchPickerOpen(false);
13664
14376
  }
13665
14377
  }, { passive: true });
13666
14378
  document.addEventListener("pointermove", (event) => {
@@ -13678,7 +14390,7 @@ function isTextEntryTarget(target) {
13678
14390
 
13679
14391
  function shouldHandleNativeAppShortcut(event) {
13680
14392
  if (event.defaultPrevented) return false;
13681
- if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.nativeCommandDialog?.open || elements.appRunnerInfoDialog?.open) return false;
14393
+ if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open || elements.nativeCommandDialog?.open || elements.appRunnerInfoDialog?.open) return false;
13682
14394
  return event.target === elements.promptInput || !isTextEntryTarget(event.target);
13683
14395
  }
13684
14396
 
@@ -13737,7 +14449,7 @@ window.addEventListener("focus", () => scheduleForegroundReconcile("window focus
13737
14449
  window.addEventListener("online", () => scheduleForegroundReconcile("network online", 0));
13738
14450
  window.addEventListener("keydown", (event) => {
13739
14451
  if (event.key !== "Escape") return;
13740
- if (elements.dialog?.open || elements.pathPickerDialog?.open) return;
14452
+ if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open) return;
13741
14453
  if (publishMenuOpen) {
13742
14454
  setPublishMenuOpen(false);
13743
14455
  return;
@@ -13775,6 +14487,7 @@ window.addEventListener("keydown", (event) => {
13775
14487
  if (isFooterPickerOpen()) {
13776
14488
  setFooterModelPickerOpen(false);
13777
14489
  setFooterThinkingPickerOpen(false);
14490
+ setFooterBranchPickerOpen(false);
13778
14491
  return;
13779
14492
  }
13780
14493
  if (!elements.commandSuggest.hidden) {
@@ -13801,6 +14514,18 @@ window.addEventListener("keydown", (event) => {
13801
14514
  }
13802
14515
  });
13803
14516
 
14517
+ elements.gitChangesRefreshButton?.addEventListener("click", refreshGitChangesDialog);
14518
+ elements.gitChangesCloseButton?.addEventListener("click", closeGitChangesDialog);
14519
+ elements.gitChangesBody?.addEventListener("scroll", updateGitChangesCurrentFileHeader, { passive: true });
14520
+ elements.gitChangesDialog?.addEventListener("cancel", (event) => {
14521
+ event.preventDefault();
14522
+ closeGitChangesDialog();
14523
+ });
14524
+ elements.gitChangesDialog?.addEventListener("close", () => {
14525
+ gitChangesRequestSerial += 1;
14526
+ gitChangesState = { ...gitChangesState, loading: false };
14527
+ });
14528
+
13804
14529
  elements.refreshCodexUsageButton?.addEventListener("click", () => {
13805
14530
  refreshCodexUsage({ forceAuthRefresh: true }).finally(() => scheduleRefreshCodexUsage());
13806
14531
  });