@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/bin/pi-webui.mjs +219 -0
- package/package.json +1 -1
- package/public/app.js +731 -6
- package/public/index.html +20 -2
- package/public/styles.css +343 -0
- package/tests/mobile-static.test.mjs +22 -0
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)
|
|
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)
|
|
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
|
});
|