@firstpick/pi-package-webui 0.3.6 → 0.3.8
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/README.md +2 -1
- package/WEBUI_TUI_NATIVE_PARITY.json +22 -22
- package/bin/pi-webui.mjs +452 -110
- package/images/WebUI_v0.3.7.png +0 -0
- package/index.ts +15 -4
- package/lib/auth-actions.mjs +81 -0
- package/lib/native-command-adapter.mjs +220 -0
- package/lib/session-actions.mjs +134 -0
- package/lib/temp-artifacts.mjs +34 -0
- package/lib/trust-boundaries.mjs +141 -0
- package/package.json +8 -4
- package/public/app.js +1278 -99
- package/public/index.html +22 -4
- package/public/service-worker.js +23 -9
- package/public/styles.css +454 -0
- package/start-webui.sh +6 -5
- package/tests/fixtures/fake-pi.mjs +73 -0
- package/tests/http-endpoints-harness.test.mjs +146 -0
- package/tests/mobile-static.test.mjs +66 -21
- package/tests/native-parity-harness.test.mjs +147 -0
- package/tests/native-parity.test.mjs +25 -6
- package/tests/run-all.mjs +19 -0
- package/tests/session-auth-harness.test.mjs +140 -0
- package/tests/temp-artifacts-harness.test.mjs +38 -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"),
|
|
@@ -202,6 +209,7 @@ let streamTextRenderTimer = null;
|
|
|
202
209
|
let streamToolCallSeen = false;
|
|
203
210
|
let streamThinkingBubble = null;
|
|
204
211
|
let streamThinking = null;
|
|
212
|
+
let streamMessageActive = false;
|
|
205
213
|
let runIndicatorBubble = null;
|
|
206
214
|
let runIndicatorText = null;
|
|
207
215
|
let runIndicatorMeta = null;
|
|
@@ -219,6 +227,9 @@ let foregroundReconcileTimer = null;
|
|
|
219
227
|
let eventSource = null;
|
|
220
228
|
let activeDialog = null;
|
|
221
229
|
let activeGitPrDialogResolve = null;
|
|
230
|
+
let gitChangesState = { loading: false, error: "", data: null, tabId: null };
|
|
231
|
+
let gitChangesRequestSerial = 0;
|
|
232
|
+
const gitChangesUntrackedContentRequests = new Set();
|
|
222
233
|
let nativeCommandTabId = null;
|
|
223
234
|
let pathPickerState = null;
|
|
224
235
|
let firstTerminalCwdPromptShown = false;
|
|
@@ -305,6 +316,10 @@ let chatUserScrollIntentUntil = 0;
|
|
|
305
316
|
let mobileFooterExpanded = false;
|
|
306
317
|
let footerModelPickerOpen = false;
|
|
307
318
|
let footerThinkingPickerOpen = false;
|
|
319
|
+
let footerAutoCompactionToggleInFlight = false;
|
|
320
|
+
let footerBranchPickerOpen = false;
|
|
321
|
+
let footerBranchPickerState = { loading: false, error: "", branches: [], current: "", root: "", switching: "", tabId: null };
|
|
322
|
+
let footerBranchPickerRequestSerial = 0;
|
|
308
323
|
let publishMenuOpen = false;
|
|
309
324
|
let maxVisualViewportHeight = 0;
|
|
310
325
|
let abortRequestInFlight = false;
|
|
@@ -341,6 +356,7 @@ const GIT_FOOTER_WEBUI_STATUS_KEY = "git-footer-webui";
|
|
|
341
356
|
const GIT_FOOTER_WEBUI_PAYLOAD_TYPE = "firstpick.git-footer-status.footer";
|
|
342
357
|
const GIT_FOOTER_WEBUI_PAYLOAD_VERSION = 1;
|
|
343
358
|
const GIT_FOOTER_WEBUI_PAYLOAD_CACHE_KEY = "pi-webui-git-footer-webui-payload-cache";
|
|
359
|
+
const GIT_CHANGES_RENDER_ROW_LIMIT = 4000;
|
|
344
360
|
const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
|
|
345
361
|
const PROMPT_HISTORY_STORAGE_KEY = "pi-webui-prompt-history";
|
|
346
362
|
const PROMPT_LIST_STORAGE_KEY = "pi-webui-prompt-lists";
|
|
@@ -410,6 +426,7 @@ const optionalFeatureAvailability = {
|
|
|
410
426
|
gitWorkflow: false,
|
|
411
427
|
releaseNpm: false,
|
|
412
428
|
releaseAur: false,
|
|
429
|
+
safetyGuard: false,
|
|
413
430
|
statsCommand: false,
|
|
414
431
|
gitFooterStatus: false,
|
|
415
432
|
tuiSkillsCommand: false,
|
|
@@ -439,6 +456,13 @@ const OPTIONAL_FEATURES = [
|
|
|
439
456
|
capabilityLabel: "/release-aur",
|
|
440
457
|
description: "Publish menu action, setup helpers, skills, and AUR release widgets.",
|
|
441
458
|
},
|
|
459
|
+
{
|
|
460
|
+
id: "safetyGuard",
|
|
461
|
+
label: "Safety guard",
|
|
462
|
+
packageName: "@firstpick/pi-extension-safety-guard",
|
|
463
|
+
capabilityLabel: "/safety-guard command or safety-guard status event",
|
|
464
|
+
description: "Interactive guardrails for dangerous bash commands and protected file edits.",
|
|
465
|
+
},
|
|
442
466
|
{
|
|
443
467
|
id: "tuiSkillsCommand",
|
|
444
468
|
label: "TUI Skills command",
|
|
@@ -489,6 +513,7 @@ const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
|
489
513
|
["pr", "gitWorkflow"],
|
|
490
514
|
["release-npm", "releaseNpm"],
|
|
491
515
|
["release-aur", "releaseAur"],
|
|
516
|
+
["safety-guard", "safetyGuard"],
|
|
492
517
|
["skills", "tuiSkillsCommand"],
|
|
493
518
|
["tools", "tuiToolsCommand"],
|
|
494
519
|
["stats", "statsCommand"],
|
|
@@ -626,20 +651,22 @@ const GIT_WORKFLOW_ACTIVE_INDEX = {
|
|
|
626
651
|
done: 4,
|
|
627
652
|
};
|
|
628
653
|
const GIT_WORKFLOW_CREATE_PR_TOOLTIP = [
|
|
629
|
-
"Create PR:",
|
|
654
|
+
"Create PR branch:",
|
|
630
655
|
"1. Ask Pi to generate a type/feature-name branch from staged changes.",
|
|
631
656
|
"2. Read dev/COMMIT/staged-branch-name.txt.",
|
|
632
657
|
"3. Let you confirm or edit the generated branch name.",
|
|
633
658
|
"4. Run git switch -c <branch>.",
|
|
634
|
-
"5. Return here
|
|
659
|
+
"5. Return here to commit short or long on that branch.",
|
|
660
|
+
"6. Push and Create PR will push upstream, run /pr, let you review, then run gh pr create.",
|
|
635
661
|
].join("\n");
|
|
636
662
|
const GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP = [
|
|
637
|
-
"Manual branch:",
|
|
663
|
+
"Manual PR branch:",
|
|
638
664
|
"1. Skip agent branch-name generation.",
|
|
639
665
|
"2. Prefill a branch from the commit message if possible.",
|
|
640
666
|
"3. Let you type or edit the type/feature-name branch name.",
|
|
641
667
|
"4. Run git switch -c <branch>.",
|
|
642
|
-
"5. Return here
|
|
668
|
+
"5. Return here to commit short or long on that branch.",
|
|
669
|
+
"6. Push and Create PR will push upstream, run /pr, let you review, then run gh pr create.",
|
|
643
670
|
].join("\n");
|
|
644
671
|
|
|
645
672
|
function make(tag, className, text) {
|
|
@@ -1480,10 +1507,11 @@ function updateComposerModeButtons() {
|
|
|
1480
1507
|
}
|
|
1481
1508
|
|
|
1482
1509
|
function isFooterPickerOpen() {
|
|
1483
|
-
return footerModelPickerOpen || footerThinkingPickerOpen;
|
|
1510
|
+
return footerModelPickerOpen || footerThinkingPickerOpen || footerBranchPickerOpen;
|
|
1484
1511
|
}
|
|
1485
1512
|
|
|
1486
1513
|
function footerActivePickerTarget() {
|
|
1514
|
+
if (footerBranchPickerOpen) return elements.statusBar.querySelector(".footer-branch.footer-meta-action");
|
|
1487
1515
|
if (footerThinkingPickerOpen) return elements.statusBar.querySelector(".footer-thinking.footer-meta-action");
|
|
1488
1516
|
if (footerModelPickerOpen) return elements.statusBar.querySelector(".footer-model.footer-meta-action, .footer-tui-model");
|
|
1489
1517
|
return null;
|
|
@@ -1534,6 +1562,7 @@ function setMobileFooterExpanded(expanded) {
|
|
|
1534
1562
|
if (mobileFooterExpanded && isFooterPickerOpen()) {
|
|
1535
1563
|
footerModelPickerOpen = false;
|
|
1536
1564
|
footerThinkingPickerOpen = false;
|
|
1565
|
+
footerBranchPickerOpen = false;
|
|
1537
1566
|
document.body.classList.remove("footer-model-picker-open");
|
|
1538
1567
|
elements.statusBar.querySelectorAll(".footer-model-picker").forEach((node) => node.remove());
|
|
1539
1568
|
}
|
|
@@ -1637,6 +1666,7 @@ function updateVisualViewportVars() {
|
|
|
1637
1666
|
setMobileTabsExpanded(false);
|
|
1638
1667
|
setMobileFooterExpanded(false);
|
|
1639
1668
|
setFooterModelPickerOpen(false);
|
|
1669
|
+
setFooterBranchPickerOpen(false);
|
|
1640
1670
|
syncMobileChatToBottomForInput();
|
|
1641
1671
|
}
|
|
1642
1672
|
updateFooterModelPickerPosition();
|
|
@@ -1836,11 +1866,22 @@ function attachMessageCopyButton(bubble, message, body) {
|
|
|
1836
1866
|
return button;
|
|
1837
1867
|
}
|
|
1838
1868
|
|
|
1869
|
+
function safeHttpUrl(value, base = window.location.href) {
|
|
1870
|
+
const text = String(value || "").trim();
|
|
1871
|
+
if (!text) return "";
|
|
1872
|
+
try {
|
|
1873
|
+
const url = new URL(text, base);
|
|
1874
|
+
return url.protocol === "http:" || url.protocol === "https:" ? url.href : "";
|
|
1875
|
+
} catch {
|
|
1876
|
+
return "";
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1839
1880
|
function triggerNativeDownload(download) {
|
|
1840
|
-
const url =
|
|
1881
|
+
const url = safeHttpUrl(download?.url);
|
|
1841
1882
|
if (!url) return false;
|
|
1842
1883
|
const anchor = document.createElement("a");
|
|
1843
|
-
anchor.href =
|
|
1884
|
+
anchor.href = url;
|
|
1844
1885
|
anchor.download = String(download.fileName || "");
|
|
1845
1886
|
anchor.rel = "noopener";
|
|
1846
1887
|
anchor.hidden = true;
|
|
@@ -3576,7 +3617,7 @@ function restoreActiveDraft() {
|
|
|
3576
3617
|
|
|
3577
3618
|
function focusPromptInput({ defer = false } = {}) {
|
|
3578
3619
|
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;
|
|
3620
|
+
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
3621
|
try {
|
|
3581
3622
|
elements.promptInput.focus({ preventScroll: true });
|
|
3582
3623
|
} catch {
|
|
@@ -3942,6 +3983,8 @@ async function switchTab(tabId) {
|
|
|
3942
3983
|
setMobileTabsExpanded(false);
|
|
3943
3984
|
footerModelPickerOpen = false;
|
|
3944
3985
|
footerThinkingPickerOpen = false;
|
|
3986
|
+
footerBranchPickerOpen = false;
|
|
3987
|
+
footerBranchPickerRequestSerial += 1;
|
|
3945
3988
|
saveActiveDraft();
|
|
3946
3989
|
const tabContext = setActiveTabId(tabId, { remember: true });
|
|
3947
3990
|
resetActiveTabUi();
|
|
@@ -4645,11 +4688,15 @@ function footerStatsCostDisplay(stats = latestStats) {
|
|
|
4645
4688
|
return `$${Number(stats.cost || 0).toFixed(3)} (${footerCostAuthLabel()})`;
|
|
4646
4689
|
}
|
|
4647
4690
|
|
|
4691
|
+
function footerAutoCompactionEnabled(state = currentState) {
|
|
4692
|
+
return state?.autoCompactionEnabled !== false;
|
|
4693
|
+
}
|
|
4694
|
+
|
|
4648
4695
|
function footerContextDisplayWithAuto(value, state = currentState) {
|
|
4649
4696
|
const text = cleanStatusText(value);
|
|
4650
4697
|
if (!text) return "";
|
|
4651
4698
|
const withoutAuto = text.replace(/\s*\(auto\)\s*$/i, "");
|
|
4652
|
-
return state
|
|
4699
|
+
return footerAutoCompactionEnabled(state) ? `${withoutAuto} (auto)` : withoutAuto;
|
|
4653
4700
|
}
|
|
4654
4701
|
|
|
4655
4702
|
function footerStatsContextDisplay(stats = latestStats) {
|
|
@@ -4662,6 +4709,48 @@ function footerStatsContextDisplay(stats = latestStats) {
|
|
|
4662
4709
|
return footerContextDisplayWithAuto(`${percent}/${formatFooterTokenCount(contextWindow)}`);
|
|
4663
4710
|
}
|
|
4664
4711
|
|
|
4712
|
+
function footerAutoCompactionToggleAction(state = currentState) {
|
|
4713
|
+
return `Click to ${footerAutoCompactionEnabled(state) ? "disable" : "enable"} auto-compaction.`;
|
|
4714
|
+
}
|
|
4715
|
+
|
|
4716
|
+
async function toggleFooterAutoCompaction(tabContext = activeTabContext()) {
|
|
4717
|
+
if (footerAutoCompactionToggleInFlight || !tabContext.tabId) return;
|
|
4718
|
+
const previousState = currentState;
|
|
4719
|
+
const enabled = !footerAutoCompactionEnabled(previousState);
|
|
4720
|
+
footerAutoCompactionToggleInFlight = true;
|
|
4721
|
+
if (isCurrentTabContext(tabContext) && currentState) {
|
|
4722
|
+
currentState = { ...currentState, autoCompactionEnabled: enabled };
|
|
4723
|
+
renderStatus();
|
|
4724
|
+
}
|
|
4725
|
+
try {
|
|
4726
|
+
await api("/api/auto-compaction", { method: "POST", body: { enabled }, tabId: tabContext.tabId });
|
|
4727
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
4728
|
+
addEvent(`Auto-compaction ${enabled ? "enabled" : "disabled"}`, "info");
|
|
4729
|
+
try {
|
|
4730
|
+
await refreshState(tabContext);
|
|
4731
|
+
} catch (error) {
|
|
4732
|
+
if (isCurrentTabContext(tabContext)) addEvent(`Auto-compaction updated, but state refresh failed: ${error.message || String(error)}`, "warn");
|
|
4733
|
+
}
|
|
4734
|
+
} catch (error) {
|
|
4735
|
+
if (isCurrentTabContext(tabContext)) {
|
|
4736
|
+
if (previousState) currentState = previousState;
|
|
4737
|
+
addEvent(error.message || String(error), "error");
|
|
4738
|
+
renderStatus();
|
|
4739
|
+
}
|
|
4740
|
+
} finally {
|
|
4741
|
+
footerAutoCompactionToggleInFlight = false;
|
|
4742
|
+
if (isCurrentTabContext(tabContext)) renderStatus();
|
|
4743
|
+
}
|
|
4744
|
+
}
|
|
4745
|
+
|
|
4746
|
+
function applyGitFooterContextToggleOptions(chip, options) {
|
|
4747
|
+
if (chip?.key !== "context") return "";
|
|
4748
|
+
options.onClick = () => toggleFooterAutoCompaction();
|
|
4749
|
+
options.ariaPressed = footerAutoCompactionEnabled();
|
|
4750
|
+
if (footerAutoCompactionToggleInFlight) options.ariaBusy = true;
|
|
4751
|
+
return footerAutoCompactionToggleInFlight ? "Updating auto-compaction…" : footerAutoCompactionToggleAction();
|
|
4752
|
+
}
|
|
4753
|
+
|
|
4665
4754
|
function fallbackFooterStats() {
|
|
4666
4755
|
return [footerStatsTokensDisplay(), footerStatsCostDisplay(), footerStatsContextDisplay()].filter(Boolean);
|
|
4667
4756
|
}
|
|
@@ -4774,7 +4863,14 @@ function applyFooterTooltip(node, tooltip, options = {}) {
|
|
|
4774
4863
|
}
|
|
4775
4864
|
|
|
4776
4865
|
function footerMetric(icon, label, value, tone = "", options = {}) {
|
|
4777
|
-
const
|
|
4866
|
+
const isAction = typeof options.onClick === "function";
|
|
4867
|
+
const node = make(isAction ? "button" : "span", ["footer-metric", tone, isAction ? "footer-metric-action" : ""].filter(Boolean).join(" "));
|
|
4868
|
+
if (isAction) {
|
|
4869
|
+
node.type = "button";
|
|
4870
|
+
node.addEventListener("click", options.onClick);
|
|
4871
|
+
if (options.ariaPressed !== undefined) node.setAttribute("aria-pressed", options.ariaPressed ? "true" : "false");
|
|
4872
|
+
if (options.ariaBusy) node.setAttribute("aria-busy", "true");
|
|
4873
|
+
}
|
|
4778
4874
|
node.append(make("span", "footer-metric-icon", icon), make("span", "footer-metric-label", label), make("span", "footer-metric-value", value));
|
|
4779
4875
|
return applyFooterTooltip(node, options.title || `${label}: ${value}`, { align: options.tooltipAlign });
|
|
4780
4876
|
}
|
|
@@ -4816,16 +4912,26 @@ function applyFooterContextUsage(node, contextUsage) {
|
|
|
4816
4912
|
|
|
4817
4913
|
function footerMeta(label, value, className = "", options = {}) {
|
|
4818
4914
|
const isAction = typeof options.onClick === "function";
|
|
4819
|
-
const node = make(isAction ? "button" : "span",
|
|
4915
|
+
const node = make(isAction ? "button" : "span", ["footer-meta", className, isAction ? "footer-meta-action" : ""].filter(Boolean).join(" "));
|
|
4820
4916
|
if (isAction) {
|
|
4821
4917
|
node.type = "button";
|
|
4822
4918
|
node.addEventListener("click", options.onClick);
|
|
4919
|
+
if (options.ariaPressed !== undefined) node.setAttribute("aria-pressed", options.ariaPressed ? "true" : "false");
|
|
4920
|
+
if (options.ariaBusy) node.setAttribute("aria-busy", "true");
|
|
4823
4921
|
}
|
|
4824
4922
|
node.append(make("span", "footer-meta-label", label), make("span", "footer-meta-value", value));
|
|
4825
4923
|
return applyFooterTooltip(node, options.title || `${label}: ${value}`, { align: options.tooltipAlign });
|
|
4826
4924
|
}
|
|
4827
4925
|
|
|
4828
4926
|
const FOOTER_PAYLOAD_TONES = new Set(["pink", "blue", "mauve", "yellow", "green", "teal"]);
|
|
4927
|
+
const FOOTER_CHANGED_FILE_KINDS = new Set(["modified", "staged", "untracked", "conflicted"]);
|
|
4928
|
+
const FOOTER_CHANGED_FILE_KIND_ORDER = ["modified", "staged", "untracked", "conflicted"];
|
|
4929
|
+
const FOOTER_CHANGED_FILE_KIND_LABELS = {
|
|
4930
|
+
modified: "Modified",
|
|
4931
|
+
staged: "Staged",
|
|
4932
|
+
untracked: "Untracked",
|
|
4933
|
+
conflicted: "Conflicted",
|
|
4934
|
+
};
|
|
4829
4935
|
const FOOTER_META_CLASS_BY_KEY = new Map([
|
|
4830
4936
|
["cwd", "footer-workspace"],
|
|
4831
4937
|
["git", "footer-branch"],
|
|
@@ -4860,6 +4966,21 @@ function cleanFooterPayloadText(value, fallback = "", maxLength = 240) {
|
|
|
4860
4966
|
return text || fallback;
|
|
4861
4967
|
}
|
|
4862
4968
|
|
|
4969
|
+
function normalizeFooterPayloadChangedFile(value) {
|
|
4970
|
+
if (!value || typeof value !== "object") return null;
|
|
4971
|
+
const path = cleanFooterPayloadText(value.path, "", 1000);
|
|
4972
|
+
if (!path) return null;
|
|
4973
|
+
const kind = FOOTER_CHANGED_FILE_KINDS.has(value.kind) ? value.kind : "modified";
|
|
4974
|
+
const file = {
|
|
4975
|
+
kind,
|
|
4976
|
+
path,
|
|
4977
|
+
status: cleanFooterPayloadText(value.status, "", 12),
|
|
4978
|
+
};
|
|
4979
|
+
const oldPath = cleanFooterPayloadText(value.oldPath, "", 1000);
|
|
4980
|
+
if (oldPath) file.oldPath = oldPath;
|
|
4981
|
+
return file;
|
|
4982
|
+
}
|
|
4983
|
+
|
|
4863
4984
|
function normalizeFooterPayloadChip(value, index) {
|
|
4864
4985
|
if (!value || typeof value !== "object") return null;
|
|
4865
4986
|
const key = cleanFooterPayloadText(value.key, `item-${index}`).replace(/[^a-z0-9_.:-]/gi, "-").slice(0, 64) || `item-${index}`;
|
|
@@ -4872,6 +4993,10 @@ function normalizeFooterPayloadChip(value, index) {
|
|
|
4872
4993
|
tone: FOOTER_PAYLOAD_TONES.has(value.tone) ? value.tone : "",
|
|
4873
4994
|
title: cleanFooterPayloadText(value.title, "", 4000),
|
|
4874
4995
|
};
|
|
4996
|
+
if (Array.isArray(value.files)) {
|
|
4997
|
+
const files = value.files.map(normalizeFooterPayloadChangedFile).filter(Boolean).slice(0, 80);
|
|
4998
|
+
if (files.length) chip.files = files;
|
|
4999
|
+
}
|
|
4875
5000
|
if (value.contextUsage && typeof value.contextUsage === "object") {
|
|
4876
5001
|
const percent = typeof value.contextUsage.percent === "number" ? value.contextUsage.percent : Number.NaN;
|
|
4877
5002
|
const contextWindow = Number(value.contextUsage.contextWindow);
|
|
@@ -5034,11 +5159,77 @@ function renderTuiFooterLine({ cwd, cwdTitle, message = "", stats = [], model =
|
|
|
5034
5159
|
return line;
|
|
5035
5160
|
}
|
|
5036
5161
|
|
|
5037
|
-
function
|
|
5038
|
-
const
|
|
5039
|
-
|
|
5040
|
-
|
|
5162
|
+
function insertChangedFilePathReference(path) {
|
|
5163
|
+
const input = elements.promptInput;
|
|
5164
|
+
if (!input) return;
|
|
5165
|
+
const reference = formatPathReference(path);
|
|
5166
|
+
const start = input.selectionStart ?? input.value.length;
|
|
5167
|
+
const end = input.selectionEnd ?? start;
|
|
5168
|
+
const before = input.value.slice(0, start);
|
|
5169
|
+
const after = input.value.slice(end);
|
|
5170
|
+
const prefix = before && !/\s$/.test(before) ? " " : "";
|
|
5171
|
+
const suffix = after && !/^\s/.test(after) ? " " : "";
|
|
5172
|
+
input.value = `${before}${prefix}${reference}${suffix}${after}`;
|
|
5173
|
+
const cursor = before.length + prefix.length + reference.length + suffix.length;
|
|
5174
|
+
input.setSelectionRange(cursor, cursor);
|
|
5175
|
+
input.focus();
|
|
5176
|
+
resizePromptInput();
|
|
5177
|
+
addEvent(`Added ${reference} to the prompt`, "info");
|
|
5178
|
+
}
|
|
5179
|
+
|
|
5180
|
+
function changedFileDisplayPath(file) {
|
|
5181
|
+
if (!file?.oldPath) return file?.path || "";
|
|
5182
|
+
return `${file.oldPath} → ${file.path}`;
|
|
5183
|
+
}
|
|
5184
|
+
|
|
5185
|
+
function renderChangedFileButton(file) {
|
|
5186
|
+
const button = make("button", `footer-changed-file ${file.kind}`.trim());
|
|
5187
|
+
button.type = "button";
|
|
5188
|
+
button.title = `Add ${file.path} as an @ reference`;
|
|
5189
|
+
button.addEventListener("click", (event) => {
|
|
5190
|
+
event.preventDefault();
|
|
5191
|
+
event.stopPropagation();
|
|
5192
|
+
insertChangedFilePathReference(file.path);
|
|
5041
5193
|
});
|
|
5194
|
+
button.append(
|
|
5195
|
+
make("span", "footer-changed-file-status", file.status || ""),
|
|
5196
|
+
make("span", "footer-changed-file-path", changedFileDisplayPath(file)),
|
|
5197
|
+
);
|
|
5198
|
+
return button;
|
|
5199
|
+
}
|
|
5200
|
+
|
|
5201
|
+
function renderChangedFilesGroup(kind, files) {
|
|
5202
|
+
if (!files.length) return null;
|
|
5203
|
+
const group = make("span", "footer-changed-files-group");
|
|
5204
|
+
group.append(make("span", "footer-changed-files-heading", `${FOOTER_CHANGED_FILE_KIND_LABELS[kind] || kind} (${files.length})`));
|
|
5205
|
+
const list = make("span", "footer-changed-files-list");
|
|
5206
|
+
list.append(...files.map(renderChangedFileButton));
|
|
5207
|
+
group.append(list);
|
|
5208
|
+
return group;
|
|
5209
|
+
}
|
|
5210
|
+
|
|
5211
|
+
function applyFooterChangedFilesDropdown(node, chip) {
|
|
5212
|
+
if (chip?.key !== "changes" || !Array.isArray(chip.files) || chip.files.length === 0) return node;
|
|
5213
|
+
node.classList.add("footer-changes-with-files");
|
|
5214
|
+
node.tabIndex = 0;
|
|
5215
|
+
node.removeAttribute("data-tooltip");
|
|
5216
|
+
node.setAttribute("aria-label", `changes: ${chip.value}. Hover or focus to choose changed files. Click a file to add it as an @ reference.`);
|
|
5217
|
+
|
|
5218
|
+
const popover = make("span", "footer-changed-files-popover");
|
|
5219
|
+
popover.append(make("span", "footer-changed-files-title", "Changed files"));
|
|
5220
|
+
for (const kind of FOOTER_CHANGED_FILE_KIND_ORDER) {
|
|
5221
|
+
const group = renderChangedFilesGroup(kind, chip.files.filter((file) => file.kind === kind));
|
|
5222
|
+
if (group) popover.append(group);
|
|
5223
|
+
}
|
|
5224
|
+
node.append(popover);
|
|
5225
|
+
return node;
|
|
5226
|
+
}
|
|
5227
|
+
|
|
5228
|
+
function renderGitFooterPayloadMetric(chip) {
|
|
5229
|
+
const options = { tooltipAlign: gitFooterTooltipAlign(chip) };
|
|
5230
|
+
const action = applyGitFooterContextToggleOptions(chip, options);
|
|
5231
|
+
options.title = gitFooterPayloadTooltip(chip, { action });
|
|
5232
|
+
const node = footerMetric(chip.icon || "•", chip.label, chip.value, chip.tone ? `tone-${chip.tone}` : "", options);
|
|
5042
5233
|
return chip.contextUsage ? applyFooterContextUsage(node, chip.contextUsage) : node;
|
|
5043
5234
|
}
|
|
5044
5235
|
|
|
@@ -5048,6 +5239,12 @@ function renderGitFooterPayloadMeta(chip, tab) {
|
|
|
5048
5239
|
if (chip.key === "cwd" && tab) {
|
|
5049
5240
|
options.onClick = changeActiveTabCwd;
|
|
5050
5241
|
action = `Click to change the working directory for ${tab.title}.`;
|
|
5242
|
+
} else if (chip.key === "git" && chip.value !== "no repo") {
|
|
5243
|
+
options.onClick = () => setFooterBranchPickerOpen(!footerBranchPickerOpen);
|
|
5244
|
+
action = "Click to switch to another local branch.";
|
|
5245
|
+
} else if (chip.key === "changes") {
|
|
5246
|
+
options.onClick = openGitChangesDialog;
|
|
5247
|
+
action = "Click to view the current git diff.";
|
|
5051
5248
|
} else if (chip.key === "model") {
|
|
5052
5249
|
options.onClick = () => setFooterModelPickerOpen(!footerModelPickerOpen);
|
|
5053
5250
|
action = "Click to choose another model.";
|
|
@@ -5055,9 +5252,15 @@ function renderGitFooterPayloadMeta(chip, tab) {
|
|
|
5055
5252
|
options.onClick = () => setFooterThinkingPickerOpen(!footerThinkingPickerOpen);
|
|
5056
5253
|
action = "Click to change thinking effort.";
|
|
5057
5254
|
}
|
|
5255
|
+
action = applyGitFooterContextToggleOptions(chip, options) || action;
|
|
5058
5256
|
options.title = gitFooterPayloadTooltip(chip, { action });
|
|
5059
5257
|
options.tooltipAlign = gitFooterTooltipAlign(chip);
|
|
5060
5258
|
const node = footerMeta(chip.label, chip.value, footerMetaClassForPayload(chip), options);
|
|
5259
|
+
applyFooterChangedFilesDropdown(node, chip);
|
|
5260
|
+
if (chip.key === "git" && options.onClick) {
|
|
5261
|
+
node.setAttribute("aria-haspopup", "listbox");
|
|
5262
|
+
node.setAttribute("aria-expanded", footerBranchPickerOpen ? "true" : "false");
|
|
5263
|
+
}
|
|
5061
5264
|
return chip.contextUsage ? applyFooterContextUsage(node, chip.contextUsage) : node;
|
|
5062
5265
|
}
|
|
5063
5266
|
|
|
@@ -5083,10 +5286,450 @@ function renderGitFooterPayload(payload) {
|
|
|
5083
5286
|
elements.statusBar.append(row1, row2);
|
|
5084
5287
|
if (footerModelPickerOpen) elements.statusBar.append(renderFooterModelPicker());
|
|
5085
5288
|
if (footerThinkingPickerOpen) elements.statusBar.append(renderFooterThinkingPicker());
|
|
5289
|
+
if (footerBranchPickerOpen) elements.statusBar.append(renderFooterBranchPicker());
|
|
5086
5290
|
setMobileFooterExpanded(mobileFooterExpanded);
|
|
5087
5291
|
updateFooterModelPickerPosition();
|
|
5088
5292
|
}
|
|
5089
5293
|
|
|
5294
|
+
function cleanGitDiffPath(value = "") {
|
|
5295
|
+
let text = String(value || "").trim();
|
|
5296
|
+
if (!text || text === "/dev/null") return "";
|
|
5297
|
+
if ((text.startsWith("a/") || text.startsWith("b/")) && text.length > 2) text = text.slice(2);
|
|
5298
|
+
return text;
|
|
5299
|
+
}
|
|
5300
|
+
|
|
5301
|
+
function gitDiffPathFromHeader(line) {
|
|
5302
|
+
const match = String(line || "").match(/^diff --git\s+(.+?)\s+(.+)$/);
|
|
5303
|
+
return cleanGitDiffPath(match?.[2] || match?.[1] || "");
|
|
5304
|
+
}
|
|
5305
|
+
|
|
5306
|
+
function parseGitUnifiedDiff(diffText = "") {
|
|
5307
|
+
const normalized = String(diffText || "").replace(/\r\n?/g, "\n");
|
|
5308
|
+
const lines = normalized.split("\n");
|
|
5309
|
+
const files = [];
|
|
5310
|
+
let file = null;
|
|
5311
|
+
let hunk = null;
|
|
5312
|
+
let oldLineNumber = 0;
|
|
5313
|
+
let newLineNumber = 0;
|
|
5314
|
+
let deleteBuffer = [];
|
|
5315
|
+
let addBuffer = [];
|
|
5316
|
+
|
|
5317
|
+
const flushChangeRows = () => {
|
|
5318
|
+
if (!hunk || (!deleteBuffer.length && !addBuffer.length)) return;
|
|
5319
|
+
if (file) {
|
|
5320
|
+
file.deletions += deleteBuffer.length;
|
|
5321
|
+
file.additions += addBuffer.length;
|
|
5322
|
+
}
|
|
5323
|
+
const rowCount = Math.max(deleteBuffer.length, addBuffer.length);
|
|
5324
|
+
for (let i = 0; i < rowCount; i++) {
|
|
5325
|
+
const left = deleteBuffer[i] || null;
|
|
5326
|
+
const right = addBuffer[i] || null;
|
|
5327
|
+
hunk.rows.push({
|
|
5328
|
+
type: left && right ? "changed" : left ? "removed" : "added",
|
|
5329
|
+
oldNumber: left?.number ?? "",
|
|
5330
|
+
newNumber: right?.number ?? "",
|
|
5331
|
+
left: left?.text ?? "",
|
|
5332
|
+
right: right?.text ?? "",
|
|
5333
|
+
});
|
|
5334
|
+
}
|
|
5335
|
+
deleteBuffer = [];
|
|
5336
|
+
addBuffer = [];
|
|
5337
|
+
};
|
|
5338
|
+
|
|
5339
|
+
const finishFile = () => {
|
|
5340
|
+
flushChangeRows();
|
|
5341
|
+
if (!file) return;
|
|
5342
|
+
file.path = file.newPath || file.oldPath || file.headerPath || "diff";
|
|
5343
|
+
files.push(file);
|
|
5344
|
+
file = null;
|
|
5345
|
+
hunk = null;
|
|
5346
|
+
};
|
|
5347
|
+
|
|
5348
|
+
for (let index = 0; index < lines.length; index++) {
|
|
5349
|
+
const line = lines[index] || "";
|
|
5350
|
+
if (index === lines.length - 1 && !line && normalized.endsWith("\n")) continue;
|
|
5351
|
+
|
|
5352
|
+
if (line.startsWith("diff --git ")) {
|
|
5353
|
+
finishFile();
|
|
5354
|
+
file = { path: "", oldPath: "", newPath: "", headerPath: gitDiffPathFromHeader(line), headers: [line], hunks: [], additions: 0, deletions: 0 };
|
|
5355
|
+
continue;
|
|
5356
|
+
}
|
|
5357
|
+
|
|
5358
|
+
if (!file) {
|
|
5359
|
+
if (!line.trim()) continue;
|
|
5360
|
+
file = { path: "diff", oldPath: "", newPath: "", headerPath: "diff", headers: [], hunks: [], additions: 0, deletions: 0 };
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5363
|
+
if (line.startsWith("@@ ")) {
|
|
5364
|
+
flushChangeRows();
|
|
5365
|
+
const match = line.match(/^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
|
|
5366
|
+
oldLineNumber = Number.parseInt(match?.[1] || "0", 10) || 0;
|
|
5367
|
+
newLineNumber = Number.parseInt(match?.[2] || "0", 10) || 0;
|
|
5368
|
+
hunk = { header: line, rows: [] };
|
|
5369
|
+
file.hunks.push(hunk);
|
|
5370
|
+
continue;
|
|
5371
|
+
}
|
|
5372
|
+
|
|
5373
|
+
if (!hunk) {
|
|
5374
|
+
file.headers.push(line);
|
|
5375
|
+
if (line.startsWith("--- ")) file.oldPath = cleanGitDiffPath(line.slice(4));
|
|
5376
|
+
if (line.startsWith("+++ ")) file.newPath = cleanGitDiffPath(line.slice(4));
|
|
5377
|
+
continue;
|
|
5378
|
+
}
|
|
5379
|
+
|
|
5380
|
+
if (line.startsWith("-")) {
|
|
5381
|
+
deleteBuffer.push({ number: oldLineNumber, text: line.slice(1) });
|
|
5382
|
+
oldLineNumber += 1;
|
|
5383
|
+
continue;
|
|
5384
|
+
}
|
|
5385
|
+
if (line.startsWith("+")) {
|
|
5386
|
+
addBuffer.push({ number: newLineNumber, text: line.slice(1) });
|
|
5387
|
+
newLineNumber += 1;
|
|
5388
|
+
continue;
|
|
5389
|
+
}
|
|
5390
|
+
|
|
5391
|
+
flushChangeRows();
|
|
5392
|
+
if (line.startsWith(" ")) {
|
|
5393
|
+
const text = line.slice(1);
|
|
5394
|
+
hunk.rows.push({ type: "context", oldNumber: oldLineNumber, newNumber: newLineNumber, left: text, right: text });
|
|
5395
|
+
oldLineNumber += 1;
|
|
5396
|
+
newLineNumber += 1;
|
|
5397
|
+
} else if (line.startsWith("\\")) {
|
|
5398
|
+
hunk.rows.push({ type: "meta", oldNumber: "", newNumber: "", left: line, right: line });
|
|
5399
|
+
} else {
|
|
5400
|
+
hunk.rows.push({ type: "meta", oldNumber: "", newNumber: "", left: line, right: line });
|
|
5401
|
+
}
|
|
5402
|
+
}
|
|
5403
|
+
|
|
5404
|
+
finishFile();
|
|
5405
|
+
return files;
|
|
5406
|
+
}
|
|
5407
|
+
|
|
5408
|
+
function gitChangesChip(label, value, className = "") {
|
|
5409
|
+
const chip = make("div", `git-changes-chip ${className}`.trim());
|
|
5410
|
+
chip.append(make("span", "git-changes-chip-label", label), make("span", "git-changes-chip-value", String(value ?? "—")));
|
|
5411
|
+
return chip;
|
|
5412
|
+
}
|
|
5413
|
+
|
|
5414
|
+
function renderGitChangesOverview(data) {
|
|
5415
|
+
const summary = data?.summary || {};
|
|
5416
|
+
const untrackedCount = Array.isArray(data?.untracked) ? data.untracked.length : Number(summary.untracked || 0);
|
|
5417
|
+
const overview = make("div", "git-changes-overview");
|
|
5418
|
+
overview.append(
|
|
5419
|
+
gitChangesChip("repo", data?.root || "—", "wide"),
|
|
5420
|
+
gitChangesChip("branch", data?.branch || "detached"),
|
|
5421
|
+
gitChangesChip("staged", summary.staged || 0, "success"),
|
|
5422
|
+
gitChangesChip("modified", summary.unstaged || 0, "warning"),
|
|
5423
|
+
gitChangesChip("untracked", untrackedCount, "muted"),
|
|
5424
|
+
gitChangesChip("conflicts", summary.conflicted || 0, (summary.conflicted || 0) > 0 ? "danger" : "muted"),
|
|
5425
|
+
);
|
|
5426
|
+
return overview;
|
|
5427
|
+
}
|
|
5428
|
+
|
|
5429
|
+
function renderGitDiffRow(row) {
|
|
5430
|
+
const node = make("div", `git-diff-row ${row.type || "context"}`.trim());
|
|
5431
|
+
node.append(
|
|
5432
|
+
make("span", "git-diff-line-number old", row.oldNumber === "" ? "" : String(row.oldNumber)),
|
|
5433
|
+
make("code", "git-diff-line old", row.left ?? ""),
|
|
5434
|
+
make("span", "git-diff-line-number new", row.newNumber === "" ? "" : String(row.newNumber)),
|
|
5435
|
+
make("code", "git-diff-line new", row.right ?? ""),
|
|
5436
|
+
);
|
|
5437
|
+
return node;
|
|
5438
|
+
}
|
|
5439
|
+
|
|
5440
|
+
function renderGitDiffGrid(file) {
|
|
5441
|
+
const grid = make("div", "git-diff-grid");
|
|
5442
|
+
const rowLimit = file.renderRowLimit ?? GIT_CHANGES_RENDER_ROW_LIMIT;
|
|
5443
|
+
let renderedRows = 0;
|
|
5444
|
+
let truncated = false;
|
|
5445
|
+
for (const hunk of file.hunks || []) {
|
|
5446
|
+
if (renderedRows >= rowLimit) {
|
|
5447
|
+
truncated = true;
|
|
5448
|
+
break;
|
|
5449
|
+
}
|
|
5450
|
+
grid.append(renderGitDiffRow({ type: "hunk", oldNumber: "", newNumber: "", left: hunk.header, right: hunk.header }));
|
|
5451
|
+
renderedRows += 1;
|
|
5452
|
+
for (const row of hunk.rows || []) {
|
|
5453
|
+
if (renderedRows >= rowLimit) {
|
|
5454
|
+
truncated = true;
|
|
5455
|
+
break;
|
|
5456
|
+
}
|
|
5457
|
+
grid.append(renderGitDiffRow(row));
|
|
5458
|
+
renderedRows += 1;
|
|
5459
|
+
}
|
|
5460
|
+
if (truncated) break;
|
|
5461
|
+
}
|
|
5462
|
+
if (truncated) {
|
|
5463
|
+
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." }));
|
|
5464
|
+
}
|
|
5465
|
+
return grid;
|
|
5466
|
+
}
|
|
5467
|
+
|
|
5468
|
+
function renderGitDiffFile(file) {
|
|
5469
|
+
const details = make("details", `git-diff-file ${file.className || ""}`.trim());
|
|
5470
|
+
details.open = true;
|
|
5471
|
+
details.dataset.gitDiffFile = file.path || "diff";
|
|
5472
|
+
const summary = make("summary", "git-diff-file-summary");
|
|
5473
|
+
summary.append(
|
|
5474
|
+
make("span", "git-diff-file-name", file.path || "diff"),
|
|
5475
|
+
make("span", "git-diff-file-stats", file.statsText || `+${file.additions || 0} −${file.deletions || 0}`),
|
|
5476
|
+
);
|
|
5477
|
+
details.append(summary);
|
|
5478
|
+
if (file.hunks?.length) {
|
|
5479
|
+
details.append(renderGitDiffGrid(file));
|
|
5480
|
+
} else {
|
|
5481
|
+
details.append(make("pre", "git-diff-raw", (file.headers || []).join("\n") || "No textual diff for this file."));
|
|
5482
|
+
}
|
|
5483
|
+
return details;
|
|
5484
|
+
}
|
|
5485
|
+
|
|
5486
|
+
function renderGitDiffSection(section, files) {
|
|
5487
|
+
const key = String(section?.key || "diff").replace(/[^a-z0-9_-]/gi, "-");
|
|
5488
|
+
const wrapper = make("section", `git-diff-section git-diff-section-${key}`);
|
|
5489
|
+
const header = make("div", "git-diff-section-heading");
|
|
5490
|
+
header.append(
|
|
5491
|
+
make("div", "git-diff-section-title", section?.label || "Git diff"),
|
|
5492
|
+
make("div", "git-diff-section-meta", `${files.length} file${files.length === 1 ? "" : "s"} · ${section?.command || "git diff"}`),
|
|
5493
|
+
);
|
|
5494
|
+
wrapper.append(header, ...files.map(renderGitDiffFile));
|
|
5495
|
+
return wrapper;
|
|
5496
|
+
}
|
|
5497
|
+
|
|
5498
|
+
function normalizeGitUntrackedEntry(value) {
|
|
5499
|
+
if (typeof value === "string") return { path: value, size: 0, binary: false, content: "", contentMissing: true };
|
|
5500
|
+
if (!value || typeof value !== "object") return null;
|
|
5501
|
+
const path = String(value.path || "").trim();
|
|
5502
|
+
if (!path) return null;
|
|
5503
|
+
const hasContent = Object.prototype.hasOwnProperty.call(value, "content");
|
|
5504
|
+
const binary = value.binary === true;
|
|
5505
|
+
const error = value.error ? String(value.error) : "";
|
|
5506
|
+
return {
|
|
5507
|
+
path,
|
|
5508
|
+
size: Number(value.size || 0) || 0,
|
|
5509
|
+
binary,
|
|
5510
|
+
content: hasContent && typeof value.content === "string" ? value.content : "",
|
|
5511
|
+
contentMissing: !hasContent && !binary && !error,
|
|
5512
|
+
error,
|
|
5513
|
+
};
|
|
5514
|
+
}
|
|
5515
|
+
|
|
5516
|
+
function gitUntrackedEntries(untracked) {
|
|
5517
|
+
return Array.isArray(untracked) ? untracked.map(normalizeGitUntrackedEntry).filter(Boolean) : [];
|
|
5518
|
+
}
|
|
5519
|
+
|
|
5520
|
+
function gitUntrackedContentLines(content = "") {
|
|
5521
|
+
const normalized = String(content || "").replace(/\r\n?/g, "\n");
|
|
5522
|
+
if (!normalized) return [];
|
|
5523
|
+
const withoutFinalNewline = normalized.endsWith("\n") ? normalized.slice(0, -1) : normalized;
|
|
5524
|
+
return withoutFinalNewline ? withoutFinalNewline.split("\n") : [""];
|
|
5525
|
+
}
|
|
5526
|
+
|
|
5527
|
+
function gitUntrackedEntryToDiffFile(entry) {
|
|
5528
|
+
const lines = gitUntrackedContentLines(entry.content);
|
|
5529
|
+
return {
|
|
5530
|
+
path: entry.path,
|
|
5531
|
+
className: "git-untracked-full-file",
|
|
5532
|
+
additions: lines.length,
|
|
5533
|
+
deletions: 0,
|
|
5534
|
+
statsText: `${entry.binary ? "binary" : `+${lines.length}`} · ${formatBytes(entry.size)}`,
|
|
5535
|
+
renderRowLimit: Number.POSITIVE_INFINITY,
|
|
5536
|
+
headers: lines.length ? [] : ["Empty untracked file."],
|
|
5537
|
+
hunks: lines.length ? [{
|
|
5538
|
+
header: `@@ -0,0 +1,${lines.length} @@`,
|
|
5539
|
+
rows: lines.map((line, index) => ({ type: "added", oldNumber: "", newNumber: index + 1, left: "", right: line })),
|
|
5540
|
+
}] : [],
|
|
5541
|
+
};
|
|
5542
|
+
}
|
|
5543
|
+
|
|
5544
|
+
function renderGitUntrackedRawFile(entry) {
|
|
5545
|
+
const details = make("details", "git-diff-file git-untracked-full-file");
|
|
5546
|
+
details.open = true;
|
|
5547
|
+
details.dataset.gitDiffFile = entry.path;
|
|
5548
|
+
const summary = make("summary", "git-diff-file-summary");
|
|
5549
|
+
summary.append(
|
|
5550
|
+
make("span", "git-diff-file-name", entry.path),
|
|
5551
|
+
make("span", "git-diff-file-stats", entry.error ? "unreadable" : `binary · ${formatBytes(entry.size)}`),
|
|
5552
|
+
);
|
|
5553
|
+
details.append(summary, make("pre", "git-diff-raw", entry.error || "Binary untracked file; text preview unavailable."));
|
|
5554
|
+
return details;
|
|
5555
|
+
}
|
|
5556
|
+
|
|
5557
|
+
function renderGitUntrackedLoadingFile(entry) {
|
|
5558
|
+
const details = make("details", "git-diff-file git-untracked-full-file git-untracked-loading-file");
|
|
5559
|
+
details.open = true;
|
|
5560
|
+
details.dataset.gitDiffFile = entry.path;
|
|
5561
|
+
const summary = make("summary", "git-diff-file-summary");
|
|
5562
|
+
summary.append(make("span", "git-diff-file-name", entry.path), make("span", "git-diff-file-stats", "loading content"));
|
|
5563
|
+
details.append(summary, make("pre", "git-diff-raw", "Loading complete untracked file content…"));
|
|
5564
|
+
return details;
|
|
5565
|
+
}
|
|
5566
|
+
|
|
5567
|
+
function renderGitUntrackedFile(entry) {
|
|
5568
|
+
if (entry.contentMissing) return renderGitUntrackedLoadingFile(entry);
|
|
5569
|
+
if (entry.error || entry.binary) return renderGitUntrackedRawFile(entry);
|
|
5570
|
+
return renderGitDiffFile(gitUntrackedEntryToDiffFile(entry));
|
|
5571
|
+
}
|
|
5572
|
+
|
|
5573
|
+
function replaceGitUntrackedEntry(entry, tabId = gitChangesState.tabId) {
|
|
5574
|
+
const data = gitChangesState.data;
|
|
5575
|
+
if (!data || tabId !== gitChangesState.tabId) return;
|
|
5576
|
+
const entries = gitUntrackedEntries(data.untracked);
|
|
5577
|
+
const nextEntries = entries.map((item) => item.path === entry.path ? normalizeGitUntrackedEntry(entry) : item);
|
|
5578
|
+
gitChangesState = { ...gitChangesState, data: { ...data, untracked: nextEntries } };
|
|
5579
|
+
renderGitChangesDialog();
|
|
5580
|
+
}
|
|
5581
|
+
|
|
5582
|
+
async function loadMissingGitUntrackedContent(entry, tabId = gitChangesState.tabId) {
|
|
5583
|
+
const key = `${tabId || ""}\u0000${entry.path}`;
|
|
5584
|
+
if (!entry.contentMissing || gitChangesUntrackedContentRequests.has(key)) return;
|
|
5585
|
+
gitChangesUntrackedContentRequests.add(key);
|
|
5586
|
+
try {
|
|
5587
|
+
const response = await api(`/api/git-changes/untracked-file?path=${encodeURIComponent(entry.path)}`, { tabId });
|
|
5588
|
+
if (!response.ok) throw new Error(response.error || "Failed to load untracked file content");
|
|
5589
|
+
replaceGitUntrackedEntry(response.data, tabId);
|
|
5590
|
+
} catch (error) {
|
|
5591
|
+
replaceGitUntrackedEntry({ ...entry, contentMissing: false, error: error.message || String(error) }, tabId);
|
|
5592
|
+
} finally {
|
|
5593
|
+
gitChangesUntrackedContentRequests.delete(key);
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5596
|
+
|
|
5597
|
+
function renderGitUntrackedSection(untracked) {
|
|
5598
|
+
const entries = gitUntrackedEntries(untracked);
|
|
5599
|
+
const wrapper = make("section", "git-diff-section git-diff-section-untracked");
|
|
5600
|
+
const header = make("div", "git-diff-section-heading");
|
|
5601
|
+
header.append(
|
|
5602
|
+
make("div", "git-diff-section-title", "Untracked"),
|
|
5603
|
+
make("div", "git-diff-section-meta", `${entries.length} file${entries.length === 1 ? "" : "s"} · complete file contents`),
|
|
5604
|
+
);
|
|
5605
|
+
wrapper.append(header, ...entries.map(renderGitUntrackedFile));
|
|
5606
|
+
for (const entry of entries) {
|
|
5607
|
+
if (entry.contentMissing) queueMicrotask(() => loadMissingGitUntrackedContent(entry));
|
|
5608
|
+
}
|
|
5609
|
+
return wrapper;
|
|
5610
|
+
}
|
|
5611
|
+
|
|
5612
|
+
function renderGitCurrentFileHeader() {
|
|
5613
|
+
const header = make("div", "git-current-file-header");
|
|
5614
|
+
header.append(make("span", "git-current-file-label", "Current file"), make("span", "git-current-file-name", "—"));
|
|
5615
|
+
return header;
|
|
5616
|
+
}
|
|
5617
|
+
|
|
5618
|
+
function updateGitChangesCurrentFileHeader() {
|
|
5619
|
+
const body = elements.gitChangesBody;
|
|
5620
|
+
const header = body?.querySelector(".git-current-file-header");
|
|
5621
|
+
const name = header?.querySelector(".git-current-file-name");
|
|
5622
|
+
if (!body || !header || !name) return;
|
|
5623
|
+
const files = Array.from(body.querySelectorAll(".git-diff-file[data-git-diff-file]"));
|
|
5624
|
+
if (!files.length) {
|
|
5625
|
+
name.textContent = "—";
|
|
5626
|
+
return;
|
|
5627
|
+
}
|
|
5628
|
+
const bodyRect = body.getBoundingClientRect();
|
|
5629
|
+
const headerRect = header.getBoundingClientRect();
|
|
5630
|
+
const markerY = Math.min(bodyRect.bottom - 1, Math.max(bodyRect.top, headerRect.bottom + 8));
|
|
5631
|
+
let current = files[0];
|
|
5632
|
+
for (const file of files) {
|
|
5633
|
+
const rect = file.getBoundingClientRect();
|
|
5634
|
+
if (rect.top <= markerY && rect.bottom > markerY) {
|
|
5635
|
+
current = file;
|
|
5636
|
+
break;
|
|
5637
|
+
}
|
|
5638
|
+
if (rect.top <= markerY) current = file;
|
|
5639
|
+
else break;
|
|
5640
|
+
}
|
|
5641
|
+
name.textContent = current?.dataset.gitDiffFile || "—";
|
|
5642
|
+
}
|
|
5643
|
+
|
|
5644
|
+
function gitChangesGeneratedLabel(data) {
|
|
5645
|
+
const timestamp = Date.parse(data?.generatedAt || "");
|
|
5646
|
+
if (!Number.isFinite(timestamp)) return "";
|
|
5647
|
+
return `Updated ${new Date(timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", second: "2-digit" })}`;
|
|
5648
|
+
}
|
|
5649
|
+
|
|
5650
|
+
function renderGitChangesDialog() {
|
|
5651
|
+
if (!elements.gitChangesDialog || !elements.gitChangesBody) return;
|
|
5652
|
+
const { loading, error, data } = gitChangesState;
|
|
5653
|
+
if (elements.gitChangesTitle) elements.gitChangesTitle.textContent = "Uncommitted Changes";
|
|
5654
|
+
if (elements.gitChangesSubtitle) elements.gitChangesSubtitle.textContent = data?.root ? `${data.branch || "detached"} · ${data.root}` : "Current tab git diff";
|
|
5655
|
+
if (elements.gitChangesRefreshButton) {
|
|
5656
|
+
elements.gitChangesRefreshButton.disabled = loading;
|
|
5657
|
+
elements.gitChangesRefreshButton.textContent = loading ? "Refreshing…" : "Refresh";
|
|
5658
|
+
}
|
|
5659
|
+
if (elements.gitChangesStatus) {
|
|
5660
|
+
elements.gitChangesStatus.className = `git-changes-status ${error ? "error" : "muted"}`;
|
|
5661
|
+
elements.gitChangesStatus.textContent = error || (loading ? "Loading git diff…" : data ? gitChangesGeneratedLabel(data) : "");
|
|
5662
|
+
elements.gitChangesStatus.hidden = !elements.gitChangesStatus.textContent;
|
|
5663
|
+
}
|
|
5664
|
+
|
|
5665
|
+
const body = elements.gitChangesBody;
|
|
5666
|
+
body.replaceChildren();
|
|
5667
|
+
if (loading && !data) {
|
|
5668
|
+
body.append(make("div", "git-changes-empty", "Loading git diff…"));
|
|
5669
|
+
return;
|
|
5670
|
+
}
|
|
5671
|
+
if (error) {
|
|
5672
|
+
body.append(make("div", "git-changes-empty error", error));
|
|
5673
|
+
return;
|
|
5674
|
+
}
|
|
5675
|
+
if (!data) {
|
|
5676
|
+
body.append(make("div", "git-changes-empty", "Open from the footer CHANGES chip to load the current git diff."));
|
|
5677
|
+
return;
|
|
5678
|
+
}
|
|
5679
|
+
|
|
5680
|
+
body.append(renderGitChangesOverview(data));
|
|
5681
|
+
const parsedSections = (Array.isArray(data.sections) ? data.sections : [])
|
|
5682
|
+
.map((section) => ({ section, files: parseGitUnifiedDiff(section.diff || "") }))
|
|
5683
|
+
.filter((entry) => entry.files.length > 0);
|
|
5684
|
+
const untracked = gitUntrackedEntries(data.untracked);
|
|
5685
|
+
const hasVisibleFiles = parsedSections.length > 0 || untracked.length > 0;
|
|
5686
|
+
if (hasVisibleFiles) body.append(renderGitCurrentFileHeader());
|
|
5687
|
+
for (const entry of parsedSections) body.append(renderGitDiffSection(entry.section, entry.files));
|
|
5688
|
+
if (untracked.length) body.append(renderGitUntrackedSection(untracked));
|
|
5689
|
+
if (!hasVisibleFiles) body.append(make("div", "git-changes-empty success", "Working tree is clean. No staged or unstaged diff."));
|
|
5690
|
+
if (hasVisibleFiles) requestAnimationFrame(updateGitChangesCurrentFileHeader);
|
|
5691
|
+
}
|
|
5692
|
+
|
|
5693
|
+
async function loadGitChangesDialog(tabContext = activeTabContext()) {
|
|
5694
|
+
const requestSerial = ++gitChangesRequestSerial;
|
|
5695
|
+
gitChangesUntrackedContentRequests.clear();
|
|
5696
|
+
gitChangesState = { ...gitChangesState, loading: true, error: "", tabId: tabContext.tabId || activeTabId };
|
|
5697
|
+
renderGitChangesDialog();
|
|
5698
|
+
try {
|
|
5699
|
+
const response = await api("/api/git-changes", { tabId: tabContext.tabId });
|
|
5700
|
+
if (requestSerial !== gitChangesRequestSerial) return;
|
|
5701
|
+
if (!response.ok) throw new Error(response.error || "Failed to load git changes");
|
|
5702
|
+
gitChangesState = { loading: false, error: "", data: response.data || null, tabId: tabContext.tabId || activeTabId };
|
|
5703
|
+
} catch (error) {
|
|
5704
|
+
if (requestSerial !== gitChangesRequestSerial) return;
|
|
5705
|
+
gitChangesState = { ...gitChangesState, loading: false, error: error.message || String(error) };
|
|
5706
|
+
}
|
|
5707
|
+
renderGitChangesDialog();
|
|
5708
|
+
}
|
|
5709
|
+
|
|
5710
|
+
function openGitChangesDialog() {
|
|
5711
|
+
if (!elements.gitChangesDialog) return;
|
|
5712
|
+
hideFooterTooltip();
|
|
5713
|
+
const tabContext = activeTabContext();
|
|
5714
|
+
const tabId = tabContext.tabId || activeTabId;
|
|
5715
|
+
gitChangesState = { loading: true, error: "", data: gitChangesState.tabId === tabId ? gitChangesState.data : null, tabId };
|
|
5716
|
+
renderGitChangesDialog();
|
|
5717
|
+
if (!elements.gitChangesDialog.open) elements.gitChangesDialog.showModal();
|
|
5718
|
+
loadGitChangesDialog(tabContext).catch((error) => addEvent(error.message || String(error), "error"));
|
|
5719
|
+
}
|
|
5720
|
+
|
|
5721
|
+
function refreshGitChangesDialog() {
|
|
5722
|
+
const tabContext = { tabId: gitChangesState.tabId || activeTabId };
|
|
5723
|
+
loadGitChangesDialog(tabContext).catch((error) => addEvent(error.message || String(error), "error"));
|
|
5724
|
+
}
|
|
5725
|
+
|
|
5726
|
+
function closeGitChangesDialog() {
|
|
5727
|
+
gitChangesRequestSerial += 1;
|
|
5728
|
+
gitChangesUntrackedContentRequests.clear();
|
|
5729
|
+
gitChangesState = { ...gitChangesState, loading: false };
|
|
5730
|
+
if (elements.gitChangesDialog?.open) elements.gitChangesDialog.close();
|
|
5731
|
+
}
|
|
5732
|
+
|
|
5090
5733
|
function gitFooterFallbackMessage() {
|
|
5091
5734
|
if (isOptionalFeatureDisabled("gitFooterStatus")) return "";
|
|
5092
5735
|
const tabContext = activeTabContext();
|
|
@@ -5118,13 +5761,18 @@ function renderMinimalFooter() {
|
|
|
5118
5761
|
}));
|
|
5119
5762
|
if (footerModelPickerOpen) elements.statusBar.append(renderFooterModelPicker());
|
|
5120
5763
|
if (footerThinkingPickerOpen) elements.statusBar.append(renderFooterThinkingPicker());
|
|
5764
|
+
if (footerBranchPickerOpen) elements.statusBar.append(renderFooterBranchPicker());
|
|
5121
5765
|
setMobileFooterExpanded(false);
|
|
5122
5766
|
updateFooterModelPickerPosition();
|
|
5123
5767
|
}
|
|
5124
5768
|
|
|
5125
5769
|
function setFooterModelPickerOpen(open) {
|
|
5126
5770
|
footerModelPickerOpen = !!open;
|
|
5127
|
-
if (footerModelPickerOpen)
|
|
5771
|
+
if (footerModelPickerOpen) {
|
|
5772
|
+
footerThinkingPickerOpen = false;
|
|
5773
|
+
footerBranchPickerOpen = false;
|
|
5774
|
+
footerBranchPickerRequestSerial += 1;
|
|
5775
|
+
}
|
|
5128
5776
|
if (footerModelPickerOpen && isMobileView()) {
|
|
5129
5777
|
mobileFooterExpanded = false;
|
|
5130
5778
|
document.body.classList.remove("footer-details-expanded");
|
|
@@ -5138,7 +5786,11 @@ function setFooterModelPickerOpen(open) {
|
|
|
5138
5786
|
|
|
5139
5787
|
function setFooterThinkingPickerOpen(open) {
|
|
5140
5788
|
footerThinkingPickerOpen = !!open;
|
|
5141
|
-
if (footerThinkingPickerOpen)
|
|
5789
|
+
if (footerThinkingPickerOpen) {
|
|
5790
|
+
footerModelPickerOpen = false;
|
|
5791
|
+
footerBranchPickerOpen = false;
|
|
5792
|
+
footerBranchPickerRequestSerial += 1;
|
|
5793
|
+
}
|
|
5142
5794
|
if (footerThinkingPickerOpen && isMobileView()) {
|
|
5143
5795
|
mobileFooterExpanded = false;
|
|
5144
5796
|
document.body.classList.remove("footer-details-expanded");
|
|
@@ -5150,6 +5802,239 @@ function setFooterThinkingPickerOpen(open) {
|
|
|
5150
5802
|
updateFooterModelPickerPosition();
|
|
5151
5803
|
}
|
|
5152
5804
|
|
|
5805
|
+
function normalizeFooterGitBranches(data = {}) {
|
|
5806
|
+
const current = cleanStatusText(data.current || "");
|
|
5807
|
+
const seen = new Set();
|
|
5808
|
+
const branches = [];
|
|
5809
|
+
for (const item of Array.isArray(data.branches) ? data.branches : []) {
|
|
5810
|
+
const name = cleanStatusText(typeof item === "string" ? item : item?.name);
|
|
5811
|
+
if (!name || seen.has(name)) continue;
|
|
5812
|
+
seen.add(name);
|
|
5813
|
+
branches.push({ name, current: Boolean(item?.current) || (!!current && name === current) });
|
|
5814
|
+
}
|
|
5815
|
+
return {
|
|
5816
|
+
root: cleanFooterPayloadText(data.root, "", 4000),
|
|
5817
|
+
current,
|
|
5818
|
+
branches,
|
|
5819
|
+
};
|
|
5820
|
+
}
|
|
5821
|
+
|
|
5822
|
+
function applyOptimisticGitFooterBranch(branch, tabContext = activeTabContext()) {
|
|
5823
|
+
const nextBranch = cleanStatusText(branch);
|
|
5824
|
+
if (!nextBranch) return;
|
|
5825
|
+
const raw = statusEntries.get(GIT_FOOTER_WEBUI_STATUS_KEY) || readCachedGitFooterWebuiPayloadRaw();
|
|
5826
|
+
const payload = parseGitFooterWebuiPayloadRaw(raw);
|
|
5827
|
+
if (!payload) return;
|
|
5828
|
+
const nextPayload = {
|
|
5829
|
+
type: GIT_FOOTER_WEBUI_PAYLOAD_TYPE,
|
|
5830
|
+
version: GIT_FOOTER_WEBUI_PAYLOAD_VERSION,
|
|
5831
|
+
generatedAt: Date.now(),
|
|
5832
|
+
main: payload.main,
|
|
5833
|
+
meta: payload.meta.map((chip) => chip.key === "git" ? { ...chip, value: nextBranch, title: `git branch: ${nextBranch}` } : chip),
|
|
5834
|
+
};
|
|
5835
|
+
const nextRaw = JSON.stringify(nextPayload);
|
|
5836
|
+
statusEntries.set(GIT_FOOTER_WEBUI_STATUS_KEY, nextRaw);
|
|
5837
|
+
cacheGitFooterWebuiPayload(nextRaw, tabContext.tabId);
|
|
5838
|
+
}
|
|
5839
|
+
|
|
5840
|
+
async function loadFooterBranchPicker(tabContext = activeTabContext()) {
|
|
5841
|
+
const requestSerial = ++footerBranchPickerRequestSerial;
|
|
5842
|
+
const tabId = tabContext.tabId || activeTabId;
|
|
5843
|
+
footerBranchPickerState = {
|
|
5844
|
+
loading: true,
|
|
5845
|
+
error: "",
|
|
5846
|
+
branches: footerBranchPickerState.tabId === tabId ? footerBranchPickerState.branches : [],
|
|
5847
|
+
current: footerBranchPickerState.tabId === tabId ? footerBranchPickerState.current : "",
|
|
5848
|
+
root: footerBranchPickerState.tabId === tabId ? footerBranchPickerState.root : "",
|
|
5849
|
+
switching: "",
|
|
5850
|
+
tabId,
|
|
5851
|
+
};
|
|
5852
|
+
if (isCurrentTabContext(tabContext)) {
|
|
5853
|
+
renderFooter();
|
|
5854
|
+
updateFooterModelPickerPosition();
|
|
5855
|
+
}
|
|
5856
|
+
try {
|
|
5857
|
+
const response = await api("/api/git-branches", { tabId });
|
|
5858
|
+
if (requestSerial !== footerBranchPickerRequestSerial || !footerBranchPickerOpen || !isCurrentTabContext(tabContext)) return;
|
|
5859
|
+
if (!response.ok) throw new Error(response.error || "Failed to load git branches");
|
|
5860
|
+
footerBranchPickerState = { loading: false, error: "", switching: "", tabId, ...normalizeFooterGitBranches(response.data || {}) };
|
|
5861
|
+
} catch (error) {
|
|
5862
|
+
if (requestSerial !== footerBranchPickerRequestSerial || !footerBranchPickerOpen || !isCurrentTabContext(tabContext)) return;
|
|
5863
|
+
footerBranchPickerState = { ...footerBranchPickerState, loading: false, switching: "", error: error.message || String(error) };
|
|
5864
|
+
}
|
|
5865
|
+
if (isCurrentTabContext(tabContext)) {
|
|
5866
|
+
renderFooter();
|
|
5867
|
+
updateFooterModelPickerPosition();
|
|
5868
|
+
}
|
|
5869
|
+
}
|
|
5870
|
+
|
|
5871
|
+
function setFooterBranchPickerOpen(open) {
|
|
5872
|
+
footerBranchPickerOpen = !!open;
|
|
5873
|
+
if (footerBranchPickerOpen) {
|
|
5874
|
+
footerModelPickerOpen = false;
|
|
5875
|
+
footerThinkingPickerOpen = false;
|
|
5876
|
+
if (isMobileView()) {
|
|
5877
|
+
mobileFooterExpanded = false;
|
|
5878
|
+
document.body.classList.remove("footer-details-expanded");
|
|
5879
|
+
setComposerActionsOpen(false);
|
|
5880
|
+
setMobileTabsExpanded(false);
|
|
5881
|
+
}
|
|
5882
|
+
loadFooterBranchPicker(activeTabContext()).catch((error) => addEvent(error.message || String(error), "error"));
|
|
5883
|
+
} else {
|
|
5884
|
+
footerBranchPickerRequestSerial += 1;
|
|
5885
|
+
}
|
|
5886
|
+
document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
|
|
5887
|
+
renderFooter();
|
|
5888
|
+
updateFooterModelPickerPosition();
|
|
5889
|
+
}
|
|
5890
|
+
|
|
5891
|
+
function pathLooksInside(parentPath, childPath) {
|
|
5892
|
+
const normalizePath = (value) => String(value || "").replace(/\\+/g, "/").replace(/\/+$/, "");
|
|
5893
|
+
const parent = normalizePath(parentPath);
|
|
5894
|
+
const child = normalizePath(childPath);
|
|
5895
|
+
return !!parent && !!child && (child === parent || child.startsWith(`${parent}/`));
|
|
5896
|
+
}
|
|
5897
|
+
|
|
5898
|
+
function footerBranchActiveAgentTabs(tabContext = activeTabContext()) {
|
|
5899
|
+
const active = activeTab();
|
|
5900
|
+
const activeCwd = latestWorkspace?.cwd || active?.cwd || "";
|
|
5901
|
+
const root = footerBranchPickerState.root || "";
|
|
5902
|
+
return tabs.filter((tab) => {
|
|
5903
|
+
const sameWorktree = root ? pathLooksInside(root, tab.cwd) : !!activeCwd && tab.cwd === activeCwd;
|
|
5904
|
+
if (!sameWorktree) return false;
|
|
5905
|
+
if (tab.id === tabContext.tabId) return currentState?.isStreaming || currentState?.isCompacting || tabHasActiveAgent(tab);
|
|
5906
|
+
return tabHasActiveAgent(tab);
|
|
5907
|
+
});
|
|
5908
|
+
}
|
|
5909
|
+
|
|
5910
|
+
function footerBranchAgentWarningLines(tabContext = activeTabContext()) {
|
|
5911
|
+
const busyTabs = footerBranchActiveAgentTabs(tabContext);
|
|
5912
|
+
if (!busyTabs.length) return [];
|
|
5913
|
+
const list = busyTabs.slice(0, 4).map((tab) => `- ${tab.title || tab.id}`).join("\n");
|
|
5914
|
+
const extra = busyTabs.length > 4 ? `\n- … +${busyTabs.length - 4} more` : "";
|
|
5915
|
+
return [
|
|
5916
|
+
"",
|
|
5917
|
+
`WARNING: ${busyTabs.length === 1 ? "An agent is" : "Agents are"} still running or waiting for input in this Git working tree:`,
|
|
5918
|
+
`${list}${extra}`,
|
|
5919
|
+
"Switching branches can change files underneath the running agent.",
|
|
5920
|
+
];
|
|
5921
|
+
}
|
|
5922
|
+
|
|
5923
|
+
function confirmFooterGitBranchAction(branch, { create = false, requireConfirm = false, tabContext = activeTabContext() } = {}) {
|
|
5924
|
+
const branchName = cleanStatusText(branch);
|
|
5925
|
+
const warningLines = footerBranchAgentWarningLines(tabContext);
|
|
5926
|
+
if (!requireConfirm && warningLines.length === 0) return true;
|
|
5927
|
+
const action = create ? "Create and switch to new git branch" : "Switch git branch";
|
|
5928
|
+
const message = [
|
|
5929
|
+
`${action}: ${branchName}?`,
|
|
5930
|
+
"",
|
|
5931
|
+
`Repository: ${footerBranchPickerState.root || currentGitFooterCacheCwd(tabContext.tabId) || "current tab"}`,
|
|
5932
|
+
...warningLines,
|
|
5933
|
+
"",
|
|
5934
|
+
"Continue?",
|
|
5935
|
+
].join("\n");
|
|
5936
|
+
return window.confirm(message);
|
|
5937
|
+
}
|
|
5938
|
+
|
|
5939
|
+
function promptFooterGitBranchName() {
|
|
5940
|
+
const value = window.prompt("New git branch name:", "");
|
|
5941
|
+
if (value === null) return "";
|
|
5942
|
+
return cleanStatusText(value);
|
|
5943
|
+
}
|
|
5944
|
+
|
|
5945
|
+
async function createFooterGitBranch() {
|
|
5946
|
+
const branchName = promptFooterGitBranchName();
|
|
5947
|
+
if (!branchName) return;
|
|
5948
|
+
const tabContext = activeTabContext();
|
|
5949
|
+
if (!confirmFooterGitBranchAction(branchName, { create: true, requireConfirm: true, tabContext })) return;
|
|
5950
|
+
await applyFooterGitBranch(branchName, { create: true, tabContext, skipConfirm: true });
|
|
5951
|
+
}
|
|
5952
|
+
|
|
5953
|
+
async function applyFooterGitBranch(branch, { create = false, tabContext = activeTabContext(), skipConfirm = false } = {}) {
|
|
5954
|
+
const branchName = cleanStatusText(branch);
|
|
5955
|
+
if (!branchName) return;
|
|
5956
|
+
const tabId = tabContext.tabId || activeTabId;
|
|
5957
|
+
if (!skipConfirm && !confirmFooterGitBranchAction(branchName, { create, tabContext })) return;
|
|
5958
|
+
try {
|
|
5959
|
+
footerBranchPickerState = { ...footerBranchPickerState, loading: true, error: "", switching: branchName, tabId };
|
|
5960
|
+
renderFooter();
|
|
5961
|
+
const response = await api("/api/git-branch", { method: "POST", body: { branch: branchName, create }, tabId });
|
|
5962
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
5963
|
+
if (!response.ok) throw new Error(response.error || `Failed to ${create ? "create and switch to" : "switch to"} ${branchName}`);
|
|
5964
|
+
const switchedBranch = cleanStatusText(response.data?.branch || branchName);
|
|
5965
|
+
footerBranchPickerOpen = false;
|
|
5966
|
+
footerBranchPickerRequestSerial += 1;
|
|
5967
|
+
footerBranchPickerState = { ...footerBranchPickerState, loading: false, switching: "", current: switchedBranch };
|
|
5968
|
+
applyOptimisticGitFooterBranch(switchedBranch, tabContext);
|
|
5969
|
+
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");
|
|
5970
|
+
requestGitFooterWebuiPayload(tabContext, { force: true });
|
|
5971
|
+
} catch (error) {
|
|
5972
|
+
if (isCurrentTabContext(tabContext)) {
|
|
5973
|
+
footerBranchPickerState = { ...footerBranchPickerState, loading: false, switching: "", error: error.message || String(error) };
|
|
5974
|
+
addEvent(error.message || String(error), "error");
|
|
5975
|
+
}
|
|
5976
|
+
} finally {
|
|
5977
|
+
if (isCurrentTabContext(tabContext)) {
|
|
5978
|
+
document.body.classList.toggle("footer-model-picker-open", isFooterPickerOpen());
|
|
5979
|
+
renderFooter();
|
|
5980
|
+
updateFooterModelPickerPosition();
|
|
5981
|
+
}
|
|
5982
|
+
}
|
|
5983
|
+
}
|
|
5984
|
+
|
|
5985
|
+
function renderFooterBranchPicker() {
|
|
5986
|
+
const picker = make("div", "footer-model-picker footer-branch-picker");
|
|
5987
|
+
picker.setAttribute("role", "listbox");
|
|
5988
|
+
picker.setAttribute("aria-label", "Git branches");
|
|
5989
|
+
const state = footerBranchPickerState;
|
|
5990
|
+
const current = state.current || "detached";
|
|
5991
|
+
picker.append(make("div", "footer-model-picker-title", "Git branches"));
|
|
5992
|
+
picker.append(make("div", "footer-model-picker-source", `${state.loading ? "Refreshing" : "Current"}: ${state.switching || current}${state.root ? ` · ${state.root}` : ""}`));
|
|
5993
|
+
|
|
5994
|
+
if (state.error) {
|
|
5995
|
+
const error = make("div", "footer-model-picker-empty error");
|
|
5996
|
+
error.append(make("strong", undefined, "Cannot load branches."), make("span", undefined, ` ${state.error}`));
|
|
5997
|
+
picker.append(error);
|
|
5998
|
+
return picker;
|
|
5999
|
+
}
|
|
6000
|
+
if (state.loading && state.branches.length === 0) {
|
|
6001
|
+
picker.append(make("div", "footer-model-picker-empty muted", "Loading local branches…"));
|
|
6002
|
+
return picker;
|
|
6003
|
+
}
|
|
6004
|
+
|
|
6005
|
+
const hasOtherBranches = state.branches.some((branch) => !branch.current && branch.name !== state.current);
|
|
6006
|
+
if (!state.loading && !hasOtherBranches) {
|
|
6007
|
+
const empty = make("div", "footer-model-picker-empty muted");
|
|
6008
|
+
empty.append(make("strong", undefined, "No other local branches available."), make("span", undefined, " Create a branch from the current HEAD to continue."));
|
|
6009
|
+
const createButton = make("button", "footer-model-option footer-branch-create-option");
|
|
6010
|
+
createButton.type = "button";
|
|
6011
|
+
createButton.append(
|
|
6012
|
+
make("span", "footer-model-option-main", "Create new branch"),
|
|
6013
|
+
make("span", "footer-model-option-name", "prompts for a name, confirms, then runs git switch -c"),
|
|
6014
|
+
);
|
|
6015
|
+
createButton.addEventListener("click", () => createFooterGitBranch().catch((error) => addEvent(error.message || String(error), "error")));
|
|
6016
|
+
picker.append(empty, createButton);
|
|
6017
|
+
}
|
|
6018
|
+
|
|
6019
|
+
for (const branch of state.branches) {
|
|
6020
|
+
const selected = branch.current || (!!state.current && branch.name === state.current);
|
|
6021
|
+
const disabled = selected || state.loading || !!state.switching;
|
|
6022
|
+
const button = make("button", `footer-model-option footer-branch-option${selected ? " active" : ""}`);
|
|
6023
|
+
button.type = "button";
|
|
6024
|
+
button.disabled = disabled;
|
|
6025
|
+
button.setAttribute("role", "option");
|
|
6026
|
+
button.setAttribute("aria-selected", selected ? "true" : "false");
|
|
6027
|
+
button.title = selected ? `Current branch: ${branch.name}` : `git switch ${branch.name}`;
|
|
6028
|
+
button.append(
|
|
6029
|
+
make("span", "footer-model-option-main", branch.name),
|
|
6030
|
+
make("span", "footer-model-option-name", selected ? "current branch" : state.switching === branch.name ? "switching…" : "switch to this branch"),
|
|
6031
|
+
);
|
|
6032
|
+
if (!disabled) button.addEventListener("click", () => applyFooterGitBranch(branch.name));
|
|
6033
|
+
picker.append(button);
|
|
6034
|
+
}
|
|
6035
|
+
return picker;
|
|
6036
|
+
}
|
|
6037
|
+
|
|
5153
6038
|
async function applyFooterModel(model) {
|
|
5154
6039
|
if (!model?.provider || !model?.id) return;
|
|
5155
6040
|
const tabContext = activeTabContext();
|
|
@@ -5605,7 +6490,7 @@ async function changeActiveTabCwd() {
|
|
|
5605
6490
|
const currentCwd = latestWorkspace?.cwd || tab.cwd || "";
|
|
5606
6491
|
const cwd = await pickCwd(tab, currentCwd);
|
|
5607
6492
|
if (!isCurrentTabContext(tabContext) || !cwd || cwd === currentCwd) return;
|
|
5608
|
-
if (!window.confirm(`Restart ${tab.title} in:\n${cwd}\n\nCurrent in-flight work in this tab will be stopped.`)) return;
|
|
6493
|
+
if (!window.confirm(`Restart ${tab.title} in:\n${cwd}\n\nCurrent in-flight work in this tab will be stopped. The conversation continues in the new directory.`)) return;
|
|
5609
6494
|
|
|
5610
6495
|
saveActiveDraft();
|
|
5611
6496
|
try {
|
|
@@ -5881,7 +6766,7 @@ function renderStatus() {
|
|
|
5881
6766
|
File: state?.sessionFile || "in-memory",
|
|
5882
6767
|
Messages: String(state?.messageCount ?? "?"),
|
|
5883
6768
|
Queue: String(state?.pendingMessageCount ?? 0),
|
|
5884
|
-
"Auto compact": state
|
|
6769
|
+
"Auto compact": footerAutoCompactionEnabled(state) ? "on" : "off",
|
|
5885
6770
|
};
|
|
5886
6771
|
for (const [key, value] of Object.entries(details)) {
|
|
5887
6772
|
elements.stateDetails.append(make("dt", undefined, key), make("dd", undefined, value));
|
|
@@ -6115,8 +7000,9 @@ function appendReleaseNpmTerminalLine(parent, line) {
|
|
|
6115
7000
|
|
|
6116
7001
|
async function sendReleaseNpmCommand(command) {
|
|
6117
7002
|
const tabContext = activeTabContext();
|
|
7003
|
+
const resolvedCommand = resolveRpcSlashCommandMessage(command);
|
|
6118
7004
|
try {
|
|
6119
|
-
await api("/api/prompt", { method: "POST", body: { message:
|
|
7005
|
+
await api("/api/prompt", { method: "POST", body: { message: resolvedCommand }, tabId: tabContext.tabId });
|
|
6120
7006
|
if (!isCurrentTabContext(tabContext)) return;
|
|
6121
7007
|
addEvent(`${command} sent`, "info");
|
|
6122
7008
|
scheduleRefreshState(120, tabContext);
|
|
@@ -10036,6 +10922,59 @@ function renderAllMessages({ preserveScroll = false } = {}) {
|
|
|
10036
10922
|
updateStickyUserPromptButton();
|
|
10037
10923
|
}
|
|
10038
10924
|
|
|
10925
|
+
function applyNativeSlashCommandEffects(response, message, tabContext = activeTabContext()) {
|
|
10926
|
+
const data = response?.data || {};
|
|
10927
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
10928
|
+
|
|
10929
|
+
for (const warning of data.warnings || []) {
|
|
10930
|
+
if (warning) addEvent(String(warning), "warn");
|
|
10931
|
+
}
|
|
10932
|
+
for (const toast of data.toasts || []) {
|
|
10933
|
+
if (toast?.message) addEvent(String(toast.message), toast.level || "info");
|
|
10934
|
+
}
|
|
10935
|
+
|
|
10936
|
+
if (data.copyText) {
|
|
10937
|
+
copyText(data.copyText).catch((error) => {
|
|
10938
|
+
addTransientMessage({
|
|
10939
|
+
role: "native",
|
|
10940
|
+
title: message.split(/\s+/, 1)[0],
|
|
10941
|
+
content: `${data.message || "Copy requested, but clipboard access failed."}\n\nClipboard access failed: ${error.message}\n\n${data.copyText}`,
|
|
10942
|
+
level: "warn",
|
|
10943
|
+
});
|
|
10944
|
+
});
|
|
10945
|
+
}
|
|
10946
|
+
|
|
10947
|
+
if (data.download && triggerNativeDownload(data.download)) {
|
|
10948
|
+
addEvent(`download started: ${data.download.fileName || data.download.url}`, "info");
|
|
10949
|
+
}
|
|
10950
|
+
|
|
10951
|
+
const cards = Array.isArray(data.cards) && data.cards.length ? data.cards : null;
|
|
10952
|
+
if (cards) {
|
|
10953
|
+
for (const card of cards) {
|
|
10954
|
+
addTransientMessage({
|
|
10955
|
+
role: "native",
|
|
10956
|
+
title: card.title || message.split(/\s+/, 1)[0],
|
|
10957
|
+
content: card.content,
|
|
10958
|
+
level: card.level || data.level || "info",
|
|
10959
|
+
});
|
|
10960
|
+
}
|
|
10961
|
+
} else if (data.message) {
|
|
10962
|
+
addTransientMessage({
|
|
10963
|
+
role: "native",
|
|
10964
|
+
title: message.split(/\s+/, 1)[0],
|
|
10965
|
+
content: data.message,
|
|
10966
|
+
level: data.level || "info",
|
|
10967
|
+
});
|
|
10968
|
+
}
|
|
10969
|
+
|
|
10970
|
+
const refresh = Array.isArray(data.refresh) ? data.refresh : ["state"];
|
|
10971
|
+
if (refresh.includes("state")) scheduleRefreshState(120, tabContext);
|
|
10972
|
+
if (refresh.includes("tabs")) scheduleRefreshTabs(300);
|
|
10973
|
+
if (refresh.includes("commands")) refreshCommands(tabContext).catch((error) => addEvent(error.message || String(error), "error"));
|
|
10974
|
+
if (refresh.includes("workspace")) scheduleRefreshState(120, tabContext);
|
|
10975
|
+
if (refresh.includes("themes")) initializeThemes().catch((error) => addEvent(error.message || String(error), "error"));
|
|
10976
|
+
}
|
|
10977
|
+
|
|
10039
10978
|
function addTransientMessage({ role = "notice", title, content, level = "info", ...details }) {
|
|
10040
10979
|
transientMessages.push({
|
|
10041
10980
|
role,
|
|
@@ -10197,10 +11136,11 @@ function setOptionsMenuOpen(open) {
|
|
|
10197
11136
|
}
|
|
10198
11137
|
|
|
10199
11138
|
function optionalFeatureIdForCommand(name) {
|
|
10200
|
-
|
|
10201
|
-
if (
|
|
10202
|
-
if (
|
|
10203
|
-
if (
|
|
11139
|
+
const baseName = commandBaseName(name);
|
|
11140
|
+
if (OPTIONAL_COMMAND_FEATURES.has(baseName)) return OPTIONAL_COMMAND_FEATURES.get(baseName);
|
|
11141
|
+
if (baseName === "release-toggle" || baseName === "release-abort" || baseName === "release-npm-logs") return "releaseNpm";
|
|
11142
|
+
if (baseName === "release-aur" || baseName.startsWith("release-aur-")) return "releaseAur";
|
|
11143
|
+
if (baseName === "stats" || baseName.startsWith("stats-") || baseName === "calibrate") return "statsCommand";
|
|
10204
11144
|
return null;
|
|
10205
11145
|
}
|
|
10206
11146
|
|
|
@@ -10215,12 +11155,51 @@ function visibleCommands() {
|
|
|
10215
11155
|
return availableCommands.filter(isCommandVisible);
|
|
10216
11156
|
}
|
|
10217
11157
|
|
|
11158
|
+
function commandBaseName(name) {
|
|
11159
|
+
return String(name || "").replace(/:\d+$/, "");
|
|
11160
|
+
}
|
|
11161
|
+
|
|
11162
|
+
function commandNameMatches(commandName, requestedName) {
|
|
11163
|
+
const commandText = String(commandName || "");
|
|
11164
|
+
const requested = String(requestedName || "");
|
|
11165
|
+
if (!requested) return false;
|
|
11166
|
+
if (commandText === requested) return true;
|
|
11167
|
+
if (!commandText.startsWith(`${requested}:`)) return false;
|
|
11168
|
+
return /^\d+$/.test(commandText.slice(requested.length + 1));
|
|
11169
|
+
}
|
|
11170
|
+
|
|
11171
|
+
function canUseCommandBaseAlias(name) {
|
|
11172
|
+
return availableCommands.some((command) => commandBaseName(command.name) === name && command.invokeName && command.duplicateCount > 1);
|
|
11173
|
+
}
|
|
11174
|
+
|
|
11175
|
+
function resolveAvailableCommand(name, { rpcOnly = false } = {}) {
|
|
11176
|
+
const requested = String(name || "").trim();
|
|
11177
|
+
if (!requested) return null;
|
|
11178
|
+
const commands = (rpcOnly ? rawAvailableCommands : availableCommands).filter((command) => !rpcOnly || command.source !== "native");
|
|
11179
|
+
const exact = commands.find((command) => command.name === requested || command.invokeName === requested || command.duplicateNames?.includes(requested));
|
|
11180
|
+
if (exact) return exact;
|
|
11181
|
+
if (!canUseCommandBaseAlias(requested)) return null;
|
|
11182
|
+
return commands.find((command) => commandNameMatches(command?.name, requested)) || null;
|
|
11183
|
+
}
|
|
11184
|
+
|
|
11185
|
+
function resolveAvailableCommandName(name, options = {}) {
|
|
11186
|
+
return resolveAvailableCommand(name, options)?.name || "";
|
|
11187
|
+
}
|
|
11188
|
+
|
|
11189
|
+
function resolveRpcSlashCommandMessage(message) {
|
|
11190
|
+
const text = String(message || "");
|
|
11191
|
+
const match = text.match(/^\/([^\s]+)([\s\S]*)$/);
|
|
11192
|
+
if (!match) return text;
|
|
11193
|
+
const resolvedName = resolveAvailableCommandName(match[1], { rpcOnly: true });
|
|
11194
|
+
return resolvedName && resolvedName !== match[1] ? `/${resolvedName}${match[2]}` : text;
|
|
11195
|
+
}
|
|
11196
|
+
|
|
10218
11197
|
function hasAvailableCommand(name) {
|
|
10219
|
-
return
|
|
11198
|
+
return !!resolveAvailableCommand(name);
|
|
10220
11199
|
}
|
|
10221
11200
|
|
|
10222
11201
|
function hasLoadedRpcCommand(name) {
|
|
10223
|
-
return
|
|
11202
|
+
return !!resolveAvailableCommand(name, { rpcOnly: true });
|
|
10224
11203
|
}
|
|
10225
11204
|
|
|
10226
11205
|
function optionalFeatureUnavailableMessage(featureId) {
|
|
@@ -10266,14 +11245,15 @@ function resetOptionalFeatureAvailability() {
|
|
|
10266
11245
|
function requestGitFooterWebuiPayload(tabContext = activeTabContext(), { force = false } = {}) {
|
|
10267
11246
|
if (!tabContext.tabId || isOptionalFeatureDisabled("gitFooterStatus")) return;
|
|
10268
11247
|
if (currentState?.isStreaming || currentState?.isCompacting) return;
|
|
10269
|
-
|
|
11248
|
+
const refreshCommand = resolveAvailableCommandName("git-footer-refresh", { rpcOnly: true });
|
|
11249
|
+
if (!refreshCommand || (!force && statusEntries.has(GIT_FOOTER_WEBUI_STATUS_KEY))) return;
|
|
10270
11250
|
if (gitFooterPayloadRefreshInFlightByTab.has(tabContext.tabId)) return;
|
|
10271
11251
|
|
|
10272
11252
|
gitFooterPayloadRefreshInFlightByTab.add(tabContext.tabId);
|
|
10273
11253
|
if (isCurrentTabContext(tabContext)) renderFooter();
|
|
10274
11254
|
api("/api/prompt", {
|
|
10275
11255
|
method: "POST",
|
|
10276
|
-
body: { message:
|
|
11256
|
+
body: { message: `/${refreshCommand} --webui-silent`, streamingBehavior: "steer" },
|
|
10277
11257
|
tabId: tabContext.tabId,
|
|
10278
11258
|
}).catch((error) => {
|
|
10279
11259
|
if (isCurrentTabContext(tabContext)) addEvent(`git footer payload refresh failed: ${error.message || String(error)}`, "warn");
|
|
@@ -10287,6 +11267,7 @@ function updateOptionalFeatureAvailability() {
|
|
|
10287
11267
|
optionalFeatureAvailability.gitWorkflow = hasAvailableCommand("git-staged-msg");
|
|
10288
11268
|
optionalFeatureAvailability.releaseNpm = hasAvailableCommand("release-npm");
|
|
10289
11269
|
optionalFeatureAvailability.releaseAur = hasAvailableCommand("release-aur");
|
|
11270
|
+
optionalFeatureAvailability.safetyGuard = hasAvailableCommand("safety-guard") || optionalFeatureAvailability.safetyGuard || statusEntries.has("safety-guard");
|
|
10290
11271
|
optionalFeatureAvailability.statsCommand = hasAvailableCommand("stats");
|
|
10291
11272
|
optionalFeatureAvailability.gitFooterStatus = hasAvailableCommand("git-footer-refresh") || optionalFeatureAvailability.gitFooterStatus || statusEntries.has("git-footer") || statusEntries.has(GIT_FOOTER_WEBUI_STATUS_KEY);
|
|
10292
11273
|
optionalFeatureAvailability.tuiSkillsCommand = hasLoadedRpcCommand("skills");
|
|
@@ -10438,9 +11419,13 @@ function runPublishWorkflow(command) {
|
|
|
10438
11419
|
setPublishMenuOpen(false);
|
|
10439
11420
|
setAppRunnerMenuOpen(false);
|
|
10440
11421
|
setOptionsMenuOpen(false);
|
|
10441
|
-
const
|
|
11422
|
+
const commandText = String(command || "");
|
|
11423
|
+
const commandWithoutSlash = commandText.replace(/^\//, "");
|
|
11424
|
+
const commandName = commandWithoutSlash.split(/\s+/)[0];
|
|
11425
|
+
const commandRest = commandWithoutSlash.slice(commandName.length);
|
|
10442
11426
|
const featureId = OPTIONAL_COMMAND_FEATURES.get(commandName);
|
|
10443
|
-
|
|
11427
|
+
const resolvedCommandName = resolveAvailableCommandName(commandName, { rpcOnly: true });
|
|
11428
|
+
if ((featureId && !isOptionalFeatureEnabled(featureId)) || !resolvedCommandName) {
|
|
10444
11429
|
const tabContext = activeTabContext();
|
|
10445
11430
|
addEvent(commandUnavailableMessage(commandName), "warn");
|
|
10446
11431
|
refreshCommands(tabContext).catch((error) => {
|
|
@@ -10448,7 +11433,7 @@ function runPublishWorkflow(command) {
|
|
|
10448
11433
|
});
|
|
10449
11434
|
return;
|
|
10450
11435
|
}
|
|
10451
|
-
sendPrompt("prompt",
|
|
11436
|
+
sendPrompt("prompt", `/${resolvedCommandName}${commandRest}`);
|
|
10452
11437
|
}
|
|
10453
11438
|
|
|
10454
11439
|
async function runNativeCommandMenu(command) {
|
|
@@ -10976,9 +11961,69 @@ function openNativeNameDialog() {
|
|
|
10976
11961
|
}
|
|
10977
11962
|
|
|
10978
11963
|
async function openNativeResumeSelector(scope = "current") {
|
|
10979
|
-
openNativeCommandDialog({ title: "/resume", message: "
|
|
11964
|
+
openNativeCommandDialog({ title: "/resume", message: "Select a session, then resume, rename metadata, or delete it.", searchPlaceholder: "Filter sessions…" });
|
|
10980
11965
|
renderNativeLoading("Loading sessions…");
|
|
10981
11966
|
const selectedScope = scope === "all" ? "all" : "current";
|
|
11967
|
+
let selectedItem = null;
|
|
11968
|
+
let resumeDeleteArmed = false;
|
|
11969
|
+
|
|
11970
|
+
const renderActions = () => {
|
|
11971
|
+
elements.nativeCommandActions.replaceChildren();
|
|
11972
|
+
const resumeButton = addNativeCommandAction("Resume", async () => {
|
|
11973
|
+
if (!selectedItem || selectedItem.disabled) return;
|
|
11974
|
+
setNativeCommandError("");
|
|
11975
|
+
try {
|
|
11976
|
+
const result = await nativeCommandApi("/api/switch-session", { method: "POST", body: { sessionPath: selectedItem.session.path } });
|
|
11977
|
+
applyResponseTab(result);
|
|
11978
|
+
addTransientMessage({ role: "native", title: "/resume", content: result.data?.message || "Resumed selected session.", level: "info" });
|
|
11979
|
+
closeNativeCommandDialog();
|
|
11980
|
+
await refreshAll();
|
|
11981
|
+
} catch (error) {
|
|
11982
|
+
setNativeCommandError(error.message || String(error));
|
|
11983
|
+
}
|
|
11984
|
+
}, selectedItem && !selectedItem.disabled ? "primary" : undefined);
|
|
11985
|
+
resumeButton.disabled = !selectedItem || selectedItem.disabled;
|
|
11986
|
+
|
|
11987
|
+
const renameButton = addNativeCommandAction("Rename", async () => {
|
|
11988
|
+
if (!selectedItem) return;
|
|
11989
|
+
const nextName = window.prompt("Session display name", selectedItem.session.name || selectedItem.label || "");
|
|
11990
|
+
if (nextName === null) return;
|
|
11991
|
+
setNativeCommandError("");
|
|
11992
|
+
try {
|
|
11993
|
+
const result = await nativeCommandApi("/api/session-rename", { method: "POST", body: { sessionPath: selectedItem.session.path, name: nextName } });
|
|
11994
|
+
addTransientMessage({ role: "native", title: "/resume", content: result.data?.message || "Renamed session metadata.", level: "info" });
|
|
11995
|
+
await openNativeResumeSelector(selectedScope);
|
|
11996
|
+
} catch (error) {
|
|
11997
|
+
setNativeCommandError(error.message || String(error));
|
|
11998
|
+
}
|
|
11999
|
+
});
|
|
12000
|
+
renameButton.disabled = !selectedItem;
|
|
12001
|
+
|
|
12002
|
+
const deleteButton = addNativeCommandAction(resumeDeleteArmed ? "Confirm delete" : "Delete", async () => {
|
|
12003
|
+
if (!selectedItem || selectedItem.disabled) return;
|
|
12004
|
+
if (!resumeDeleteArmed) {
|
|
12005
|
+
resumeDeleteArmed = true;
|
|
12006
|
+
setNativeCommandError("Delete is permanent when trash is unavailable. Click Confirm delete to proceed.");
|
|
12007
|
+
renderActions();
|
|
12008
|
+
return;
|
|
12009
|
+
}
|
|
12010
|
+
setNativeCommandError("");
|
|
12011
|
+
try {
|
|
12012
|
+
const result = await nativeCommandApi("/api/session-delete", { method: "POST", body: { sessionPath: selectedItem.session.path, confirmed: true } });
|
|
12013
|
+
addTransientMessage({ role: "native", title: "/resume", content: result.data?.message || "Session deleted.", level: "warn" });
|
|
12014
|
+
await openNativeResumeSelector(selectedScope);
|
|
12015
|
+
} catch (error) {
|
|
12016
|
+
setNativeCommandError(error.message || String(error));
|
|
12017
|
+
resumeDeleteArmed = false;
|
|
12018
|
+
renderActions();
|
|
12019
|
+
}
|
|
12020
|
+
}, resumeDeleteArmed ? "danger" : undefined);
|
|
12021
|
+
deleteButton.disabled = !selectedItem || selectedItem.disabled;
|
|
12022
|
+
|
|
12023
|
+
addNativeCommandAction(selectedScope === "all" ? "Current cwd" : "All sessions", () => openNativeResumeSelector(selectedScope === "all" ? "current" : "all"));
|
|
12024
|
+
addNativeCommandAction("Cancel", closeNativeCommandDialog);
|
|
12025
|
+
};
|
|
12026
|
+
|
|
10982
12027
|
try {
|
|
10983
12028
|
const response = await nativeCommandApi(`/api/sessions?scope=${encodeURIComponent(selectedScope)}`);
|
|
10984
12029
|
const items = (response.data?.sessions || []).map((session) => ({
|
|
@@ -10990,25 +12035,26 @@ async function openNativeResumeSelector(scope = "current") {
|
|
|
10990
12035
|
disabled: session.current,
|
|
10991
12036
|
session,
|
|
10992
12037
|
}));
|
|
10993
|
-
const render = () =>
|
|
10994
|
-
|
|
10995
|
-
|
|
10996
|
-
|
|
10997
|
-
|
|
10998
|
-
|
|
10999
|
-
|
|
11000
|
-
|
|
11001
|
-
|
|
11002
|
-
|
|
11003
|
-
}
|
|
11004
|
-
|
|
11005
|
-
|
|
11006
|
-
|
|
11007
|
-
|
|
11008
|
-
|
|
11009
|
-
|
|
11010
|
-
|
|
11011
|
-
|
|
12038
|
+
const render = () => {
|
|
12039
|
+
resumeDeleteArmed = false;
|
|
12040
|
+
renderNativeSelectorItems(items, {
|
|
12041
|
+
emptyText: selectedScope === "all" ? "No sessions match this filter." : "No sessions for this working directory match this filter.",
|
|
12042
|
+
activeId: selectedItem?.id,
|
|
12043
|
+
onSelect: (item) => {
|
|
12044
|
+
selectedItem = item;
|
|
12045
|
+
setNativeCommandError("");
|
|
12046
|
+
render();
|
|
12047
|
+
renderActions();
|
|
12048
|
+
},
|
|
12049
|
+
});
|
|
12050
|
+
};
|
|
12051
|
+
elements.nativeCommandSearch.oninput = () => {
|
|
12052
|
+
selectedItem = null;
|
|
12053
|
+
resumeDeleteArmed = false;
|
|
12054
|
+
render();
|
|
12055
|
+
renderActions();
|
|
12056
|
+
};
|
|
12057
|
+
renderActions();
|
|
11012
12058
|
render();
|
|
11013
12059
|
} catch (error) {
|
|
11014
12060
|
setNativeCommandError(error.message || String(error));
|
|
@@ -11247,14 +12293,63 @@ async function openNativeSkillsSelector() {
|
|
|
11247
12293
|
}
|
|
11248
12294
|
}
|
|
11249
12295
|
|
|
11250
|
-
function
|
|
12296
|
+
async function openNativeAuthSelector(mode) {
|
|
11251
12297
|
const command = mode === "logout" ? "/logout" : "/login";
|
|
11252
|
-
openNativeCommandDialog({
|
|
11253
|
-
|
|
11254
|
-
"
|
|
11255
|
-
"
|
|
11256
|
-
|
|
11257
|
-
|
|
12298
|
+
openNativeCommandDialog({
|
|
12299
|
+
title: command,
|
|
12300
|
+
message: mode === "logout" ? "Remove stored provider credentials from auth.json." : "Provider login still requires the Pi TUI.",
|
|
12301
|
+
searchPlaceholder: "Filter providers…",
|
|
12302
|
+
});
|
|
12303
|
+
renderNativeLoading("Loading provider auth status…");
|
|
12304
|
+
try {
|
|
12305
|
+
const response = await nativeCommandApi("/api/auth-providers");
|
|
12306
|
+
const providers = mode === "logout" ? response.data?.logoutProviders || [] : response.data?.loginProviders || [];
|
|
12307
|
+
const guidance = String(response.data?.guidance || "").trim();
|
|
12308
|
+
const items = providers.map((provider) => ({
|
|
12309
|
+
id: provider.id,
|
|
12310
|
+
label: provider.name || provider.id,
|
|
12311
|
+
description: provider.authType === "oauth" ? "OAuth / subscription" : "API key",
|
|
12312
|
+
meta: provider.status?.configured
|
|
12313
|
+
? `configured via ${provider.status.source || "stored"}`
|
|
12314
|
+
: "not configured in auth.json",
|
|
12315
|
+
badge: provider.status?.configured ? "configured" : "",
|
|
12316
|
+
provider,
|
|
12317
|
+
}));
|
|
12318
|
+
if (!items.length) {
|
|
12319
|
+
elements.nativeCommandBody.replaceChildren(make("p", "native-command-note", mode === "logout"
|
|
12320
|
+
? "No stored credentials to remove. /logout only removes credentials saved by /login."
|
|
12321
|
+
: "No providers are currently available."));
|
|
12322
|
+
if (guidance) elements.nativeCommandBody.append(make("p", "native-command-note muted", guidance));
|
|
12323
|
+
return;
|
|
12324
|
+
}
|
|
12325
|
+
const render = () => renderNativeSelectorItems(items, {
|
|
12326
|
+
emptyText: "No providers match this filter.",
|
|
12327
|
+
onSelect: async (item) => {
|
|
12328
|
+
if (mode === "login") {
|
|
12329
|
+
setNativeCommandError(`Run /login in the Pi TUI for ${item.label}. Browser login is not implemented yet.`);
|
|
12330
|
+
return;
|
|
12331
|
+
}
|
|
12332
|
+
if (!window.confirm(`Remove stored credentials for ${item.label}?`)) return;
|
|
12333
|
+
setNativeCommandError("");
|
|
12334
|
+
try {
|
|
12335
|
+
const result = await nativeCommandApi("/api/auth-logout", {
|
|
12336
|
+
method: "POST",
|
|
12337
|
+
body: { provider: item.provider.id, confirmed: true },
|
|
12338
|
+
});
|
|
12339
|
+
addTransientMessage({ role: "native", title: "/logout", content: result.data?.message || "Provider credentials removed.", level: "info" });
|
|
12340
|
+
closeNativeCommandDialog();
|
|
12341
|
+
scheduleRefreshState(120);
|
|
12342
|
+
} catch (error) {
|
|
12343
|
+
setNativeCommandError(error.message || String(error));
|
|
12344
|
+
}
|
|
12345
|
+
},
|
|
12346
|
+
});
|
|
12347
|
+
elements.nativeCommandSearch.oninput = render;
|
|
12348
|
+
render();
|
|
12349
|
+
} catch (error) {
|
|
12350
|
+
setNativeCommandError(error.message || String(error));
|
|
12351
|
+
elements.nativeCommandBody.replaceChildren();
|
|
12352
|
+
}
|
|
11258
12353
|
}
|
|
11259
12354
|
|
|
11260
12355
|
async function handleNativeSlashSelectorCommand(message, { usesPromptInput = false } = {}) {
|
|
@@ -11311,7 +12406,7 @@ async function handleNativeSlashSelectorCommand(message, { usesPromptInput = fal
|
|
|
11311
12406
|
return true;
|
|
11312
12407
|
case "login":
|
|
11313
12408
|
case "logout":
|
|
11314
|
-
|
|
12409
|
+
await openNativeAuthSelector(name);
|
|
11315
12410
|
return true;
|
|
11316
12411
|
default:
|
|
11317
12412
|
return false;
|
|
@@ -11428,6 +12523,30 @@ function resetStreamBubble() {
|
|
|
11428
12523
|
streamToolCallSeen = false;
|
|
11429
12524
|
streamThinkingBubble = null;
|
|
11430
12525
|
streamThinking = null;
|
|
12526
|
+
streamMessageActive = false;
|
|
12527
|
+
}
|
|
12528
|
+
|
|
12529
|
+
function liveStreamRenderActive() {
|
|
12530
|
+
return streamMessageActive && currentState?.isStreaming === true && Boolean(streamBubble || streamThinkingBubble || streamRawText);
|
|
12531
|
+
}
|
|
12532
|
+
|
|
12533
|
+
/**
|
|
12534
|
+
* The chat DOM was rebuilt while an assistant message is still streaming:
|
|
12535
|
+
* drop references to the detached nodes but keep stream text state, then
|
|
12536
|
+
* re-append the live thinking/text bubbles so no partial output is lost.
|
|
12537
|
+
*/
|
|
12538
|
+
function restoreStreamRenderAfterChatRebuild() {
|
|
12539
|
+
const thinkingText = streamThinking?.textContent || "";
|
|
12540
|
+
const thinkingComplete = streamThinkingBubble?.classList.contains("complete") === true;
|
|
12541
|
+
streamBubble = null;
|
|
12542
|
+
streamText = null;
|
|
12543
|
+
streamThinkingBubble = null;
|
|
12544
|
+
streamThinking = null;
|
|
12545
|
+
streamBubbleVisibleSince = 0;
|
|
12546
|
+
if (thinkingText && setStreamingThinkingText(thinkingText) && thinkingComplete) {
|
|
12547
|
+
streamThinkingBubble?.classList.add("complete");
|
|
12548
|
+
}
|
|
12549
|
+
if (stripTodoProgressLines(streamRawText, { streaming: true })) renderStreamingAssistantText();
|
|
11431
12550
|
}
|
|
11432
12551
|
|
|
11433
12552
|
function thinkingDeltaText(update) {
|
|
@@ -11612,11 +12731,12 @@ function renderNetworkStatus() {
|
|
|
11612
12731
|
const list = make("div", "network-url-list");
|
|
11613
12732
|
|
|
11614
12733
|
const addUrl = (label, url) => {
|
|
11615
|
-
|
|
12734
|
+
const href = safeHttpUrl(url);
|
|
12735
|
+
if (!href) return;
|
|
11616
12736
|
const row = make("div", "network-status-url-row");
|
|
11617
12737
|
const labelNode = make("span", "network-status-url-label", label);
|
|
11618
12738
|
const link = make("a", "network-status-url", url);
|
|
11619
|
-
link.href =
|
|
12739
|
+
link.href = href;
|
|
11620
12740
|
link.target = "_blank";
|
|
11621
12741
|
link.rel = "noreferrer";
|
|
11622
12742
|
row.append(labelNode, link);
|
|
@@ -11655,8 +12775,10 @@ async function refreshMessages(tabContext = activeTabContext()) {
|
|
|
11655
12775
|
const response = await api("/api/messages", { tabId: tabContext.tabId });
|
|
11656
12776
|
if (!isCurrentTabContext(tabContext)) return;
|
|
11657
12777
|
latestMessages = response.data?.messages || [];
|
|
11658
|
-
|
|
12778
|
+
const preserveLiveStream = liveStreamRenderActive();
|
|
12779
|
+
if (!preserveLiveStream) resetStreamBubble();
|
|
11659
12780
|
renderMessages(latestMessages);
|
|
12781
|
+
if (preserveLiveStream) restoreStreamRenderAfterChatRebuild();
|
|
11660
12782
|
markTabOutputSeen();
|
|
11661
12783
|
renderFooter();
|
|
11662
12784
|
}
|
|
@@ -11706,23 +12828,67 @@ function syncModelSelectToState() {
|
|
|
11706
12828
|
}
|
|
11707
12829
|
}
|
|
11708
12830
|
|
|
12831
|
+
function normalizedCommandIdentity(command) {
|
|
12832
|
+
return [commandBaseName(command.name), command.description, command.source, command.enabled ? "enabled" : "disabled"].join("\u0000");
|
|
12833
|
+
}
|
|
12834
|
+
|
|
12835
|
+
function combineIdenticalDuplicateCommands(commands) {
|
|
12836
|
+
const duplicateGroups = new Map();
|
|
12837
|
+
for (const command of commands) {
|
|
12838
|
+
if (commandBaseName(command.name) === command.name) continue;
|
|
12839
|
+
const key = normalizedCommandIdentity(command);
|
|
12840
|
+
if (!duplicateGroups.has(key)) duplicateGroups.set(key, []);
|
|
12841
|
+
duplicateGroups.get(key).push(command);
|
|
12842
|
+
}
|
|
12843
|
+
const combinedKeys = new Set([...duplicateGroups.entries()].filter(([, group]) => group.length > 1).map(([key]) => key));
|
|
12844
|
+
const emittedKeys = new Set();
|
|
12845
|
+
const emittedNames = new Set();
|
|
12846
|
+
const result = [];
|
|
12847
|
+
|
|
12848
|
+
for (const command of commands) {
|
|
12849
|
+
const key = normalizedCommandIdentity(command);
|
|
12850
|
+
if (!combinedKeys.has(key)) {
|
|
12851
|
+
if (emittedNames.has(command.name)) continue;
|
|
12852
|
+
emittedNames.add(command.name);
|
|
12853
|
+
result.push(command);
|
|
12854
|
+
continue;
|
|
12855
|
+
}
|
|
12856
|
+
|
|
12857
|
+
if (emittedKeys.has(key)) continue;
|
|
12858
|
+
emittedKeys.add(key);
|
|
12859
|
+
const group = duplicateGroups.get(key);
|
|
12860
|
+
const baseName = commandBaseName(command.name);
|
|
12861
|
+
const displayName = emittedNames.has(baseName) ? command.name : baseName;
|
|
12862
|
+
emittedNames.add(displayName);
|
|
12863
|
+
result.push({
|
|
12864
|
+
...command,
|
|
12865
|
+
name: displayName,
|
|
12866
|
+
invokeName: command.name,
|
|
12867
|
+
duplicateNames: group.map((item) => item.name),
|
|
12868
|
+
duplicateCount: group.length,
|
|
12869
|
+
location: command.location || `${group.length} identical loaded commands`,
|
|
12870
|
+
});
|
|
12871
|
+
}
|
|
12872
|
+
|
|
12873
|
+
return result;
|
|
12874
|
+
}
|
|
12875
|
+
|
|
11709
12876
|
function normalizeCommands(commands, { dedupe = true } = {}) {
|
|
11710
|
-
const
|
|
11711
|
-
|
|
11712
|
-
|
|
11713
|
-
|
|
11714
|
-
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
if (!dedupe) return true;
|
|
11722
|
-
if (seen.has(command.name)) return false;
|
|
11723
|
-
seen.add(command.name);
|
|
11724
|
-
return true;
|
|
12877
|
+
const normalized = (commands || [])
|
|
12878
|
+
.map((command) => {
|
|
12879
|
+
const name = String(command.name || "").trim();
|
|
12880
|
+
return {
|
|
12881
|
+
name,
|
|
12882
|
+
invokeName: name,
|
|
12883
|
+
description: String(command.description || "").trim(),
|
|
12884
|
+
source: String(command.source || "command").trim(),
|
|
12885
|
+
location: String(command.location || "").trim(),
|
|
12886
|
+
enabled: command.enabled !== false,
|
|
12887
|
+
};
|
|
11725
12888
|
})
|
|
12889
|
+
.filter((command) => command.name);
|
|
12890
|
+
|
|
12891
|
+
return (dedupe ? combineIdenticalDuplicateCommands(normalized) : normalized)
|
|
11726
12892
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
11727
12893
|
}
|
|
11728
12894
|
|
|
@@ -11789,11 +12955,13 @@ function getPathTrigger() {
|
|
|
11789
12955
|
function scoreCommandSuggestion(command, query) {
|
|
11790
12956
|
if (!query) return 0;
|
|
11791
12957
|
const q = query.toLowerCase();
|
|
11792
|
-
const
|
|
12958
|
+
const names = [command.name, command.invokeName, ...(command.duplicateNames || [])]
|
|
12959
|
+
.filter(Boolean)
|
|
12960
|
+
.map((name) => name.toLowerCase());
|
|
11793
12961
|
const description = command.description.toLowerCase();
|
|
11794
|
-
if (name === q) return 0;
|
|
11795
|
-
if (name.startsWith(q)) return 1;
|
|
11796
|
-
if (name.includes(q)) return 2;
|
|
12962
|
+
if (names.some((name) => name === q)) return 0;
|
|
12963
|
+
if (names.some((name) => name.startsWith(q))) return 1;
|
|
12964
|
+
if (names.some((name) => name.includes(q))) return 2;
|
|
11797
12965
|
if (description.includes(q)) return 3;
|
|
11798
12966
|
return Number.POSITIVE_INFINITY;
|
|
11799
12967
|
}
|
|
@@ -11813,7 +12981,7 @@ function commandSearchQuery() {
|
|
|
11813
12981
|
|
|
11814
12982
|
function commandMatchesSearch(command, query) {
|
|
11815
12983
|
if (!query) return true;
|
|
11816
|
-
return [command.name, command.description, command.source, command.location]
|
|
12984
|
+
return [command.name, command.invokeName, ...(command.duplicateNames || []), command.description, command.source, command.location]
|
|
11817
12985
|
.filter(Boolean)
|
|
11818
12986
|
.join(" ")
|
|
11819
12987
|
.toLowerCase()
|
|
@@ -12516,6 +13684,9 @@ async function runUserBashCommand(parsed, { usesPromptInput = false, targetTabId
|
|
|
12516
13684
|
const result = response.data || {};
|
|
12517
13685
|
applyResponseTab(response);
|
|
12518
13686
|
if (isCurrentTabContext(tabContext)) {
|
|
13687
|
+
for (const warning of response.warnings || []) {
|
|
13688
|
+
if (warning) addEvent(String(warning), "warn");
|
|
13689
|
+
}
|
|
12519
13690
|
addTransientMessage({
|
|
12520
13691
|
role: "bashExecution",
|
|
12521
13692
|
title: excludeFromContext ? "bash (!! complete)" : "bash (! complete)",
|
|
@@ -12596,6 +13767,7 @@ async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = acti
|
|
|
12596
13767
|
try {
|
|
12597
13768
|
const prepared = attachments.length ? await prepareAttachmentsForPrompt(attachments, targetTabId) : { images: [], uploadedFiles: [], inlineImageIds: new Set() };
|
|
12598
13769
|
message = composeMessageWithAttachments(originalMessage, prepared.uploadedFiles, prepared.inlineImageIds);
|
|
13770
|
+
if (kind === "prompt" && attachments.length === 0) message = resolveRpcSlashCommandMessage(message);
|
|
12599
13771
|
const bodyBase = { message };
|
|
12600
13772
|
if (prepared.images.length) bodyBase.images = prepared.images;
|
|
12601
13773
|
if (!message.startsWith("/")) {
|
|
@@ -12625,19 +13797,8 @@ async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = acti
|
|
|
12625
13797
|
} else if (targetStillActive && kind === "follow-up" && currentState?.isStreaming) {
|
|
12626
13798
|
setRunIndicatorActivity("Follow-up queued; current agent run is still active…");
|
|
12627
13799
|
}
|
|
12628
|
-
if (targetStillActive && response?.command === "native_slash_command"
|
|
12629
|
-
|
|
12630
|
-
await copyText(response.data.copyText);
|
|
12631
|
-
} catch (error) {
|
|
12632
|
-
response.data.message = `${response.data.message || "Copy requested, but clipboard access failed."}\n\nClipboard access failed: ${error.message}\n\n${response.data.copyText}`;
|
|
12633
|
-
response.data.level = "warn";
|
|
12634
|
-
}
|
|
12635
|
-
}
|
|
12636
|
-
if (targetStillActive && response?.command === "native_slash_command" && response.data?.download) {
|
|
12637
|
-
if (triggerNativeDownload(response.data.download)) addEvent(`download started: ${response.data.download.fileName || response.data.download.url}`, "info");
|
|
12638
|
-
}
|
|
12639
|
-
if (targetStillActive && response?.command === "native_slash_command" && response.data?.message) {
|
|
12640
|
-
addTransientMessage({ role: "native", title: message.split(/\s+/, 1)[0], content: response.data.message, level: response.data.level || "info" });
|
|
13800
|
+
if (targetStillActive && response?.command === "native_slash_command") {
|
|
13801
|
+
applyNativeSlashCommandEffects(response, message, tabContext);
|
|
12641
13802
|
}
|
|
12642
13803
|
if (usesPromptInput) {
|
|
12643
13804
|
clearAttachments(targetTabId);
|
|
@@ -12650,8 +13811,8 @@ async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = acti
|
|
|
12650
13811
|
}
|
|
12651
13812
|
if (targetStillActive) {
|
|
12652
13813
|
hideCommandSuggestions();
|
|
12653
|
-
scheduleRefreshState(120, tabContext);
|
|
12654
|
-
} else {
|
|
13814
|
+
if (response?.command !== "native_slash_command") scheduleRefreshState(120, tabContext);
|
|
13815
|
+
} else if (response?.command !== "native_slash_command") {
|
|
12655
13816
|
scheduleRefreshTabs(300);
|
|
12656
13817
|
}
|
|
12657
13818
|
} catch (error) {
|
|
@@ -12950,12 +14111,14 @@ function handleEvent(event) {
|
|
|
12950
14111
|
renderFeedbackTray();
|
|
12951
14112
|
break;
|
|
12952
14113
|
case "agent_end":
|
|
14114
|
+
streamMessageActive = false;
|
|
12953
14115
|
addEvent("agent finished");
|
|
12954
14116
|
notifyAgentDone(event.tabId || activeTabId, { activity: event.tabActivity, tabTitle: event.tabTitle });
|
|
12955
14117
|
clearContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
12956
14118
|
if (currentState) currentState = { ...currentState, isStreaming: false };
|
|
12957
14119
|
clearRunIndicatorActivity();
|
|
12958
14120
|
markTabOutputSeen();
|
|
14121
|
+
requestGitFooterWebuiPayload(tabContext, { force: true });
|
|
12959
14122
|
scheduleRefreshState();
|
|
12960
14123
|
scheduleRefreshMessages();
|
|
12961
14124
|
scheduleRefreshFooter();
|
|
@@ -12976,6 +14139,7 @@ function handleEvent(event) {
|
|
|
12976
14139
|
case "message_start":
|
|
12977
14140
|
if (event.message?.role === "assistant") {
|
|
12978
14141
|
resetStreamBubble();
|
|
14142
|
+
streamMessageActive = true;
|
|
12979
14143
|
setRunIndicatorActivity("Starting assistant message…", { scroll: false });
|
|
12980
14144
|
}
|
|
12981
14145
|
break;
|
|
@@ -12983,6 +14147,7 @@ function handleEvent(event) {
|
|
|
12983
14147
|
handleMessageUpdate(event);
|
|
12984
14148
|
break;
|
|
12985
14149
|
case "message_end":
|
|
14150
|
+
streamMessageActive = false;
|
|
12986
14151
|
if (runIndicatorIsActive()) setRunIndicatorActivity("Assistant message finished; waiting for the next step…", { scroll: false });
|
|
12987
14152
|
scheduleRefreshMessages();
|
|
12988
14153
|
scheduleRefreshState();
|
|
@@ -13661,6 +14826,7 @@ document.addEventListener("pointerdown", (event) => {
|
|
|
13661
14826
|
if (isFooterPickerOpen() && !elements.statusBar.contains(event.target)) {
|
|
13662
14827
|
setFooterModelPickerOpen(false);
|
|
13663
14828
|
setFooterThinkingPickerOpen(false);
|
|
14829
|
+
setFooterBranchPickerOpen(false);
|
|
13664
14830
|
}
|
|
13665
14831
|
}, { passive: true });
|
|
13666
14832
|
document.addEventListener("pointermove", (event) => {
|
|
@@ -13678,7 +14844,7 @@ function isTextEntryTarget(target) {
|
|
|
13678
14844
|
|
|
13679
14845
|
function shouldHandleNativeAppShortcut(event) {
|
|
13680
14846
|
if (event.defaultPrevented) return false;
|
|
13681
|
-
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.nativeCommandDialog?.open || elements.appRunnerInfoDialog?.open) return false;
|
|
14847
|
+
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open || elements.nativeCommandDialog?.open || elements.appRunnerInfoDialog?.open) return false;
|
|
13682
14848
|
return event.target === elements.promptInput || !isTextEntryTarget(event.target);
|
|
13683
14849
|
}
|
|
13684
14850
|
|
|
@@ -13737,7 +14903,7 @@ window.addEventListener("focus", () => scheduleForegroundReconcile("window focus
|
|
|
13737
14903
|
window.addEventListener("online", () => scheduleForegroundReconcile("network online", 0));
|
|
13738
14904
|
window.addEventListener("keydown", (event) => {
|
|
13739
14905
|
if (event.key !== "Escape") return;
|
|
13740
|
-
if (elements.dialog?.open || elements.pathPickerDialog?.open) return;
|
|
14906
|
+
if (elements.dialog?.open || elements.pathPickerDialog?.open || elements.gitChangesDialog?.open) return;
|
|
13741
14907
|
if (publishMenuOpen) {
|
|
13742
14908
|
setPublishMenuOpen(false);
|
|
13743
14909
|
return;
|
|
@@ -13775,6 +14941,7 @@ window.addEventListener("keydown", (event) => {
|
|
|
13775
14941
|
if (isFooterPickerOpen()) {
|
|
13776
14942
|
setFooterModelPickerOpen(false);
|
|
13777
14943
|
setFooterThinkingPickerOpen(false);
|
|
14944
|
+
setFooterBranchPickerOpen(false);
|
|
13778
14945
|
return;
|
|
13779
14946
|
}
|
|
13780
14947
|
if (!elements.commandSuggest.hidden) {
|
|
@@ -13801,6 +14968,18 @@ window.addEventListener("keydown", (event) => {
|
|
|
13801
14968
|
}
|
|
13802
14969
|
});
|
|
13803
14970
|
|
|
14971
|
+
elements.gitChangesRefreshButton?.addEventListener("click", refreshGitChangesDialog);
|
|
14972
|
+
elements.gitChangesCloseButton?.addEventListener("click", closeGitChangesDialog);
|
|
14973
|
+
elements.gitChangesBody?.addEventListener("scroll", updateGitChangesCurrentFileHeader, { passive: true });
|
|
14974
|
+
elements.gitChangesDialog?.addEventListener("cancel", (event) => {
|
|
14975
|
+
event.preventDefault();
|
|
14976
|
+
closeGitChangesDialog();
|
|
14977
|
+
});
|
|
14978
|
+
elements.gitChangesDialog?.addEventListener("close", () => {
|
|
14979
|
+
gitChangesRequestSerial += 1;
|
|
14980
|
+
gitChangesState = { ...gitChangesState, loading: false };
|
|
14981
|
+
});
|
|
14982
|
+
|
|
13804
14983
|
elements.refreshCodexUsageButton?.addEventListener("click", () => {
|
|
13805
14984
|
refreshCodexUsage({ forceAuthRefresh: true }).finally(() => scheduleRefreshCodexUsage());
|
|
13806
14985
|
});
|