@firstpick/pi-package-webui 0.3.7 → 0.3.9
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 +259 -112
- 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 +554 -94
- package/public/index.html +2 -2
- package/public/service-worker.js +23 -9
- package/public/styles.css +111 -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 +45 -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
|
@@ -209,6 +209,7 @@ let streamTextRenderTimer = null;
|
|
|
209
209
|
let streamToolCallSeen = false;
|
|
210
210
|
let streamThinkingBubble = null;
|
|
211
211
|
let streamThinking = null;
|
|
212
|
+
let streamMessageActive = false;
|
|
212
213
|
let runIndicatorBubble = null;
|
|
213
214
|
let runIndicatorText = null;
|
|
214
215
|
let runIndicatorMeta = null;
|
|
@@ -315,6 +316,7 @@ let chatUserScrollIntentUntil = 0;
|
|
|
315
316
|
let mobileFooterExpanded = false;
|
|
316
317
|
let footerModelPickerOpen = false;
|
|
317
318
|
let footerThinkingPickerOpen = false;
|
|
319
|
+
let footerAutoCompactionToggleInFlight = false;
|
|
318
320
|
let footerBranchPickerOpen = false;
|
|
319
321
|
let footerBranchPickerState = { loading: false, error: "", branches: [], current: "", root: "", switching: "", tabId: null };
|
|
320
322
|
let footerBranchPickerRequestSerial = 0;
|
|
@@ -424,6 +426,7 @@ const optionalFeatureAvailability = {
|
|
|
424
426
|
gitWorkflow: false,
|
|
425
427
|
releaseNpm: false,
|
|
426
428
|
releaseAur: false,
|
|
429
|
+
safetyGuard: false,
|
|
427
430
|
statsCommand: false,
|
|
428
431
|
gitFooterStatus: false,
|
|
429
432
|
tuiSkillsCommand: false,
|
|
@@ -453,6 +456,13 @@ const OPTIONAL_FEATURES = [
|
|
|
453
456
|
capabilityLabel: "/release-aur",
|
|
454
457
|
description: "Publish menu action, setup helpers, skills, and AUR release widgets.",
|
|
455
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
|
+
},
|
|
456
466
|
{
|
|
457
467
|
id: "tuiSkillsCommand",
|
|
458
468
|
label: "TUI Skills command",
|
|
@@ -503,6 +513,7 @@ const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
|
503
513
|
["pr", "gitWorkflow"],
|
|
504
514
|
["release-npm", "releaseNpm"],
|
|
505
515
|
["release-aur", "releaseAur"],
|
|
516
|
+
["safety-guard", "safetyGuard"],
|
|
506
517
|
["skills", "tuiSkillsCommand"],
|
|
507
518
|
["tools", "tuiToolsCommand"],
|
|
508
519
|
["stats", "statsCommand"],
|
|
@@ -640,20 +651,22 @@ const GIT_WORKFLOW_ACTIVE_INDEX = {
|
|
|
640
651
|
done: 4,
|
|
641
652
|
};
|
|
642
653
|
const GIT_WORKFLOW_CREATE_PR_TOOLTIP = [
|
|
643
|
-
"Create PR:",
|
|
654
|
+
"Create PR branch:",
|
|
644
655
|
"1. Ask Pi to generate a type/feature-name branch from staged changes.",
|
|
645
656
|
"2. Read dev/COMMIT/staged-branch-name.txt.",
|
|
646
657
|
"3. Let you confirm or edit the generated branch name.",
|
|
647
658
|
"4. Run git switch -c <branch>.",
|
|
648
|
-
"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.",
|
|
649
661
|
].join("\n");
|
|
650
662
|
const GIT_WORKFLOW_MANUAL_BRANCH_TOOLTIP = [
|
|
651
|
-
"Manual branch:",
|
|
663
|
+
"Manual PR branch:",
|
|
652
664
|
"1. Skip agent branch-name generation.",
|
|
653
665
|
"2. Prefill a branch from the commit message if possible.",
|
|
654
666
|
"3. Let you type or edit the type/feature-name branch name.",
|
|
655
667
|
"4. Run git switch -c <branch>.",
|
|
656
|
-
"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.",
|
|
657
670
|
].join("\n");
|
|
658
671
|
|
|
659
672
|
function make(tag, className, text) {
|
|
@@ -1853,11 +1866,22 @@ function attachMessageCopyButton(bubble, message, body) {
|
|
|
1853
1866
|
return button;
|
|
1854
1867
|
}
|
|
1855
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
|
+
|
|
1856
1880
|
function triggerNativeDownload(download) {
|
|
1857
|
-
const url =
|
|
1881
|
+
const url = safeHttpUrl(download?.url);
|
|
1858
1882
|
if (!url) return false;
|
|
1859
1883
|
const anchor = document.createElement("a");
|
|
1860
|
-
anchor.href =
|
|
1884
|
+
anchor.href = url;
|
|
1861
1885
|
anchor.download = String(download.fileName || "");
|
|
1862
1886
|
anchor.rel = "noopener";
|
|
1863
1887
|
anchor.hidden = true;
|
|
@@ -4664,11 +4688,15 @@ function footerStatsCostDisplay(stats = latestStats) {
|
|
|
4664
4688
|
return `$${Number(stats.cost || 0).toFixed(3)} (${footerCostAuthLabel()})`;
|
|
4665
4689
|
}
|
|
4666
4690
|
|
|
4691
|
+
function footerAutoCompactionEnabled(state = currentState) {
|
|
4692
|
+
return state?.autoCompactionEnabled !== false;
|
|
4693
|
+
}
|
|
4694
|
+
|
|
4667
4695
|
function footerContextDisplayWithAuto(value, state = currentState) {
|
|
4668
4696
|
const text = cleanStatusText(value);
|
|
4669
4697
|
if (!text) return "";
|
|
4670
4698
|
const withoutAuto = text.replace(/\s*\(auto\)\s*$/i, "");
|
|
4671
|
-
return state
|
|
4699
|
+
return footerAutoCompactionEnabled(state) ? `${withoutAuto} (auto)` : withoutAuto;
|
|
4672
4700
|
}
|
|
4673
4701
|
|
|
4674
4702
|
function footerStatsContextDisplay(stats = latestStats) {
|
|
@@ -4681,6 +4709,48 @@ function footerStatsContextDisplay(stats = latestStats) {
|
|
|
4681
4709
|
return footerContextDisplayWithAuto(`${percent}/${formatFooterTokenCount(contextWindow)}`);
|
|
4682
4710
|
}
|
|
4683
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
|
+
|
|
4684
4754
|
function fallbackFooterStats() {
|
|
4685
4755
|
return [footerStatsTokensDisplay(), footerStatsCostDisplay(), footerStatsContextDisplay()].filter(Boolean);
|
|
4686
4756
|
}
|
|
@@ -4793,7 +4863,14 @@ function applyFooterTooltip(node, tooltip, options = {}) {
|
|
|
4793
4863
|
}
|
|
4794
4864
|
|
|
4795
4865
|
function footerMetric(icon, label, value, tone = "", options = {}) {
|
|
4796
|
-
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
|
+
}
|
|
4797
4874
|
node.append(make("span", "footer-metric-icon", icon), make("span", "footer-metric-label", label), make("span", "footer-metric-value", value));
|
|
4798
4875
|
return applyFooterTooltip(node, options.title || `${label}: ${value}`, { align: options.tooltipAlign });
|
|
4799
4876
|
}
|
|
@@ -4835,16 +4912,26 @@ function applyFooterContextUsage(node, contextUsage) {
|
|
|
4835
4912
|
|
|
4836
4913
|
function footerMeta(label, value, className = "", options = {}) {
|
|
4837
4914
|
const isAction = typeof options.onClick === "function";
|
|
4838
|
-
const node = make(isAction ? "button" : "span",
|
|
4915
|
+
const node = make(isAction ? "button" : "span", ["footer-meta", className, isAction ? "footer-meta-action" : ""].filter(Boolean).join(" "));
|
|
4839
4916
|
if (isAction) {
|
|
4840
4917
|
node.type = "button";
|
|
4841
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");
|
|
4842
4921
|
}
|
|
4843
4922
|
node.append(make("span", "footer-meta-label", label), make("span", "footer-meta-value", value));
|
|
4844
4923
|
return applyFooterTooltip(node, options.title || `${label}: ${value}`, { align: options.tooltipAlign });
|
|
4845
4924
|
}
|
|
4846
4925
|
|
|
4847
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
|
+
};
|
|
4848
4935
|
const FOOTER_META_CLASS_BY_KEY = new Map([
|
|
4849
4936
|
["cwd", "footer-workspace"],
|
|
4850
4937
|
["git", "footer-branch"],
|
|
@@ -4868,7 +4955,7 @@ const GIT_FOOTER_TOOLTIP_COPY = {
|
|
|
4868
4955
|
git: "Current Git branch. detached means HEAD is not on a branch; no repo means the cwd is outside a Git work tree.",
|
|
4869
4956
|
"git-state": "Active Git operation or detached state. Finish or abort rebase/merge/cherry-pick/revert/bisect before normal commits.",
|
|
4870
4957
|
sync: "Remote tracking divergence. ↑ means local commits ahead; ↓ means remote commits to pull.",
|
|
4871
|
-
changes: "Working tree summary. 🟢 staged, ✏️ modified unstaged, ➕ untracked, ⚠️ conflicted; ✅ means no changes.",
|
|
4958
|
+
changes: "Working tree and fetched remote summary. 🟢 staged, ✏️ modified unstaged, ➕ untracked, ⚠️ conflicted; ⬇️ means fetched remote commits to pull; 🔄/✓/⚠️ fetch shows the tab git fetch state; ✅ means no changes.",
|
|
4872
4959
|
"git-extra": "Extra Git signals. 📦 stash, 🧩 dirty submodules, 🌳 worktrees, 🏷️ tag at HEAD, 🕒 last commit age, 🔓 signing mismatch.",
|
|
4873
4960
|
model: "Scoped model for this tab.",
|
|
4874
4961
|
thinking: "Reasoning/thinking effort for this tab.",
|
|
@@ -4879,6 +4966,21 @@ function cleanFooterPayloadText(value, fallback = "", maxLength = 240) {
|
|
|
4879
4966
|
return text || fallback;
|
|
4880
4967
|
}
|
|
4881
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
|
+
|
|
4882
4984
|
function normalizeFooterPayloadChip(value, index) {
|
|
4883
4985
|
if (!value || typeof value !== "object") return null;
|
|
4884
4986
|
const key = cleanFooterPayloadText(value.key, `item-${index}`).replace(/[^a-z0-9_.:-]/gi, "-").slice(0, 64) || `item-${index}`;
|
|
@@ -4891,6 +4993,10 @@ function normalizeFooterPayloadChip(value, index) {
|
|
|
4891
4993
|
tone: FOOTER_PAYLOAD_TONES.has(value.tone) ? value.tone : "",
|
|
4892
4994
|
title: cleanFooterPayloadText(value.title, "", 4000),
|
|
4893
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
|
+
}
|
|
4894
5000
|
if (value.contextUsage && typeof value.contextUsage === "object") {
|
|
4895
5001
|
const percent = typeof value.contextUsage.percent === "number" ? value.contextUsage.percent : Number.NaN;
|
|
4896
5002
|
const contextWindow = Number(value.contextUsage.contextWindow);
|
|
@@ -5053,11 +5159,77 @@ function renderTuiFooterLine({ cwd, cwdTitle, message = "", stats = [], model =
|
|
|
5053
5159
|
return line;
|
|
5054
5160
|
}
|
|
5055
5161
|
|
|
5056
|
-
function
|
|
5057
|
-
const
|
|
5058
|
-
|
|
5059
|
-
|
|
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);
|
|
5060
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);
|
|
5061
5233
|
return chip.contextUsage ? applyFooterContextUsage(node, chip.contextUsage) : node;
|
|
5062
5234
|
}
|
|
5063
5235
|
|
|
@@ -5080,9 +5252,11 @@ function renderGitFooterPayloadMeta(chip, tab) {
|
|
|
5080
5252
|
options.onClick = () => setFooterThinkingPickerOpen(!footerThinkingPickerOpen);
|
|
5081
5253
|
action = "Click to change thinking effort.";
|
|
5082
5254
|
}
|
|
5255
|
+
action = applyGitFooterContextToggleOptions(chip, options) || action;
|
|
5083
5256
|
options.title = gitFooterPayloadTooltip(chip, { action });
|
|
5084
5257
|
options.tooltipAlign = gitFooterTooltipAlign(chip);
|
|
5085
5258
|
const node = footerMeta(chip.label, chip.value, footerMetaClassForPayload(chip), options);
|
|
5259
|
+
applyFooterChangedFilesDropdown(node, chip);
|
|
5086
5260
|
if (chip.key === "git" && options.onClick) {
|
|
5087
5261
|
node.setAttribute("aria-haspopup", "listbox");
|
|
5088
5262
|
node.setAttribute("aria-expanded", footerBranchPickerOpen ? "true" : "false");
|
|
@@ -5240,10 +5414,14 @@ function gitChangesChip(label, value, className = "") {
|
|
|
5240
5414
|
function renderGitChangesOverview(data) {
|
|
5241
5415
|
const summary = data?.summary || {};
|
|
5242
5416
|
const untrackedCount = Array.isArray(data?.untracked) ? data.untracked.length : Number(summary.untracked || 0);
|
|
5417
|
+
const ahead = Number(summary.ahead || 0) || 0;
|
|
5418
|
+
const behind = Number(summary.behind || 0) || 0;
|
|
5243
5419
|
const overview = make("div", "git-changes-overview");
|
|
5244
5420
|
overview.append(
|
|
5245
5421
|
gitChangesChip("repo", data?.root || "—", "wide"),
|
|
5246
5422
|
gitChangesChip("branch", data?.branch || "detached"),
|
|
5423
|
+
gitChangesChip("ahead", ahead > 0 ? `↑${ahead}` : 0, ahead > 0 ? "warning" : "muted"),
|
|
5424
|
+
gitChangesChip("remote", behind > 0 ? `↓${behind}` : 0, behind > 0 ? "danger" : "muted"),
|
|
5247
5425
|
gitChangesChip("staged", summary.staged || 0, "success"),
|
|
5248
5426
|
gitChangesChip("modified", summary.unstaged || 0, "warning"),
|
|
5249
5427
|
gitChangesChip("untracked", untrackedCount, "muted"),
|
|
@@ -6316,7 +6494,7 @@ async function changeActiveTabCwd() {
|
|
|
6316
6494
|
const currentCwd = latestWorkspace?.cwd || tab.cwd || "";
|
|
6317
6495
|
const cwd = await pickCwd(tab, currentCwd);
|
|
6318
6496
|
if (!isCurrentTabContext(tabContext) || !cwd || cwd === currentCwd) return;
|
|
6319
|
-
if (!window.confirm(`Restart ${tab.title} in:\n${cwd}\n\nCurrent in-flight work in this tab will be stopped.`)) return;
|
|
6497
|
+
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;
|
|
6320
6498
|
|
|
6321
6499
|
saveActiveDraft();
|
|
6322
6500
|
try {
|
|
@@ -6384,6 +6562,8 @@ function scheduleRefreshFooter(delay = 300, tabContext = activeTabContext()) {
|
|
|
6384
6562
|
function formatCodexPlanType(value) {
|
|
6385
6563
|
const text = String(value || "").trim();
|
|
6386
6564
|
if (!text) return "unknown plan";
|
|
6565
|
+
const normalized = text.replace(/[\s_-]+/g, "").toLowerCase();
|
|
6566
|
+
if (normalized === "prolite") return "Usage";
|
|
6387
6567
|
return text.replace(/[_-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
6388
6568
|
}
|
|
6389
6569
|
|
|
@@ -6592,7 +6772,7 @@ function renderStatus() {
|
|
|
6592
6772
|
File: state?.sessionFile || "in-memory",
|
|
6593
6773
|
Messages: String(state?.messageCount ?? "?"),
|
|
6594
6774
|
Queue: String(state?.pendingMessageCount ?? 0),
|
|
6595
|
-
"Auto compact": state
|
|
6775
|
+
"Auto compact": footerAutoCompactionEnabled(state) ? "on" : "off",
|
|
6596
6776
|
};
|
|
6597
6777
|
for (const [key, value] of Object.entries(details)) {
|
|
6598
6778
|
elements.stateDetails.append(make("dt", undefined, key), make("dd", undefined, value));
|
|
@@ -6826,8 +7006,9 @@ function appendReleaseNpmTerminalLine(parent, line) {
|
|
|
6826
7006
|
|
|
6827
7007
|
async function sendReleaseNpmCommand(command) {
|
|
6828
7008
|
const tabContext = activeTabContext();
|
|
7009
|
+
const resolvedCommand = resolveRpcSlashCommandMessage(command);
|
|
6829
7010
|
try {
|
|
6830
|
-
await api("/api/prompt", { method: "POST", body: { message:
|
|
7011
|
+
await api("/api/prompt", { method: "POST", body: { message: resolvedCommand }, tabId: tabContext.tabId });
|
|
6831
7012
|
if (!isCurrentTabContext(tabContext)) return;
|
|
6832
7013
|
addEvent(`${command} sent`, "info");
|
|
6833
7014
|
scheduleRefreshState(120, tabContext);
|
|
@@ -10747,6 +10928,59 @@ function renderAllMessages({ preserveScroll = false } = {}) {
|
|
|
10747
10928
|
updateStickyUserPromptButton();
|
|
10748
10929
|
}
|
|
10749
10930
|
|
|
10931
|
+
function applyNativeSlashCommandEffects(response, message, tabContext = activeTabContext()) {
|
|
10932
|
+
const data = response?.data || {};
|
|
10933
|
+
if (!isCurrentTabContext(tabContext)) return;
|
|
10934
|
+
|
|
10935
|
+
for (const warning of data.warnings || []) {
|
|
10936
|
+
if (warning) addEvent(String(warning), "warn");
|
|
10937
|
+
}
|
|
10938
|
+
for (const toast of data.toasts || []) {
|
|
10939
|
+
if (toast?.message) addEvent(String(toast.message), toast.level || "info");
|
|
10940
|
+
}
|
|
10941
|
+
|
|
10942
|
+
if (data.copyText) {
|
|
10943
|
+
copyText(data.copyText).catch((error) => {
|
|
10944
|
+
addTransientMessage({
|
|
10945
|
+
role: "native",
|
|
10946
|
+
title: message.split(/\s+/, 1)[0],
|
|
10947
|
+
content: `${data.message || "Copy requested, but clipboard access failed."}\n\nClipboard access failed: ${error.message}\n\n${data.copyText}`,
|
|
10948
|
+
level: "warn",
|
|
10949
|
+
});
|
|
10950
|
+
});
|
|
10951
|
+
}
|
|
10952
|
+
|
|
10953
|
+
if (data.download && triggerNativeDownload(data.download)) {
|
|
10954
|
+
addEvent(`download started: ${data.download.fileName || data.download.url}`, "info");
|
|
10955
|
+
}
|
|
10956
|
+
|
|
10957
|
+
const cards = Array.isArray(data.cards) && data.cards.length ? data.cards : null;
|
|
10958
|
+
if (cards) {
|
|
10959
|
+
for (const card of cards) {
|
|
10960
|
+
addTransientMessage({
|
|
10961
|
+
role: "native",
|
|
10962
|
+
title: card.title || message.split(/\s+/, 1)[0],
|
|
10963
|
+
content: card.content,
|
|
10964
|
+
level: card.level || data.level || "info",
|
|
10965
|
+
});
|
|
10966
|
+
}
|
|
10967
|
+
} else if (data.message) {
|
|
10968
|
+
addTransientMessage({
|
|
10969
|
+
role: "native",
|
|
10970
|
+
title: message.split(/\s+/, 1)[0],
|
|
10971
|
+
content: data.message,
|
|
10972
|
+
level: data.level || "info",
|
|
10973
|
+
});
|
|
10974
|
+
}
|
|
10975
|
+
|
|
10976
|
+
const refresh = Array.isArray(data.refresh) ? data.refresh : ["state"];
|
|
10977
|
+
if (refresh.includes("state")) scheduleRefreshState(120, tabContext);
|
|
10978
|
+
if (refresh.includes("tabs")) scheduleRefreshTabs(300);
|
|
10979
|
+
if (refresh.includes("commands")) refreshCommands(tabContext).catch((error) => addEvent(error.message || String(error), "error"));
|
|
10980
|
+
if (refresh.includes("workspace")) scheduleRefreshState(120, tabContext);
|
|
10981
|
+
if (refresh.includes("themes")) initializeThemes().catch((error) => addEvent(error.message || String(error), "error"));
|
|
10982
|
+
}
|
|
10983
|
+
|
|
10750
10984
|
function addTransientMessage({ role = "notice", title, content, level = "info", ...details }) {
|
|
10751
10985
|
transientMessages.push({
|
|
10752
10986
|
role,
|
|
@@ -10908,10 +11142,11 @@ function setOptionsMenuOpen(open) {
|
|
|
10908
11142
|
}
|
|
10909
11143
|
|
|
10910
11144
|
function optionalFeatureIdForCommand(name) {
|
|
10911
|
-
|
|
10912
|
-
if (
|
|
10913
|
-
if (
|
|
10914
|
-
if (
|
|
11145
|
+
const baseName = commandBaseName(name);
|
|
11146
|
+
if (OPTIONAL_COMMAND_FEATURES.has(baseName)) return OPTIONAL_COMMAND_FEATURES.get(baseName);
|
|
11147
|
+
if (baseName === "release-toggle" || baseName === "release-abort" || baseName === "release-npm-logs") return "releaseNpm";
|
|
11148
|
+
if (baseName === "release-aur" || baseName.startsWith("release-aur-")) return "releaseAur";
|
|
11149
|
+
if (baseName === "stats" || baseName.startsWith("stats-") || baseName === "calibrate") return "statsCommand";
|
|
10915
11150
|
return null;
|
|
10916
11151
|
}
|
|
10917
11152
|
|
|
@@ -10926,12 +11161,51 @@ function visibleCommands() {
|
|
|
10926
11161
|
return availableCommands.filter(isCommandVisible);
|
|
10927
11162
|
}
|
|
10928
11163
|
|
|
11164
|
+
function commandBaseName(name) {
|
|
11165
|
+
return String(name || "").replace(/:\d+$/, "");
|
|
11166
|
+
}
|
|
11167
|
+
|
|
11168
|
+
function commandNameMatches(commandName, requestedName) {
|
|
11169
|
+
const commandText = String(commandName || "");
|
|
11170
|
+
const requested = String(requestedName || "");
|
|
11171
|
+
if (!requested) return false;
|
|
11172
|
+
if (commandText === requested) return true;
|
|
11173
|
+
if (!commandText.startsWith(`${requested}:`)) return false;
|
|
11174
|
+
return /^\d+$/.test(commandText.slice(requested.length + 1));
|
|
11175
|
+
}
|
|
11176
|
+
|
|
11177
|
+
function canUseCommandBaseAlias(name) {
|
|
11178
|
+
return availableCommands.some((command) => commandBaseName(command.name) === name && command.invokeName && command.duplicateCount > 1);
|
|
11179
|
+
}
|
|
11180
|
+
|
|
11181
|
+
function resolveAvailableCommand(name, { rpcOnly = false } = {}) {
|
|
11182
|
+
const requested = String(name || "").trim();
|
|
11183
|
+
if (!requested) return null;
|
|
11184
|
+
const commands = (rpcOnly ? rawAvailableCommands : availableCommands).filter((command) => !rpcOnly || command.source !== "native");
|
|
11185
|
+
const exact = commands.find((command) => command.name === requested || command.invokeName === requested || command.duplicateNames?.includes(requested));
|
|
11186
|
+
if (exact) return exact;
|
|
11187
|
+
if (!canUseCommandBaseAlias(requested)) return null;
|
|
11188
|
+
return commands.find((command) => commandNameMatches(command?.name, requested)) || null;
|
|
11189
|
+
}
|
|
11190
|
+
|
|
11191
|
+
function resolveAvailableCommandName(name, options = {}) {
|
|
11192
|
+
return resolveAvailableCommand(name, options)?.name || "";
|
|
11193
|
+
}
|
|
11194
|
+
|
|
11195
|
+
function resolveRpcSlashCommandMessage(message) {
|
|
11196
|
+
const text = String(message || "");
|
|
11197
|
+
const match = text.match(/^\/([^\s]+)([\s\S]*)$/);
|
|
11198
|
+
if (!match) return text;
|
|
11199
|
+
const resolvedName = resolveAvailableCommandName(match[1], { rpcOnly: true });
|
|
11200
|
+
return resolvedName && resolvedName !== match[1] ? `/${resolvedName}${match[2]}` : text;
|
|
11201
|
+
}
|
|
11202
|
+
|
|
10929
11203
|
function hasAvailableCommand(name) {
|
|
10930
|
-
return
|
|
11204
|
+
return !!resolveAvailableCommand(name);
|
|
10931
11205
|
}
|
|
10932
11206
|
|
|
10933
11207
|
function hasLoadedRpcCommand(name) {
|
|
10934
|
-
return
|
|
11208
|
+
return !!resolveAvailableCommand(name, { rpcOnly: true });
|
|
10935
11209
|
}
|
|
10936
11210
|
|
|
10937
11211
|
function optionalFeatureUnavailableMessage(featureId) {
|
|
@@ -10977,14 +11251,15 @@ function resetOptionalFeatureAvailability() {
|
|
|
10977
11251
|
function requestGitFooterWebuiPayload(tabContext = activeTabContext(), { force = false } = {}) {
|
|
10978
11252
|
if (!tabContext.tabId || isOptionalFeatureDisabled("gitFooterStatus")) return;
|
|
10979
11253
|
if (currentState?.isStreaming || currentState?.isCompacting) return;
|
|
10980
|
-
|
|
11254
|
+
const refreshCommand = resolveAvailableCommandName("git-footer-refresh", { rpcOnly: true });
|
|
11255
|
+
if (!refreshCommand || (!force && statusEntries.has(GIT_FOOTER_WEBUI_STATUS_KEY))) return;
|
|
10981
11256
|
if (gitFooterPayloadRefreshInFlightByTab.has(tabContext.tabId)) return;
|
|
10982
11257
|
|
|
10983
11258
|
gitFooterPayloadRefreshInFlightByTab.add(tabContext.tabId);
|
|
10984
11259
|
if (isCurrentTabContext(tabContext)) renderFooter();
|
|
10985
11260
|
api("/api/prompt", {
|
|
10986
11261
|
method: "POST",
|
|
10987
|
-
body: { message:
|
|
11262
|
+
body: { message: `/${refreshCommand} --webui-silent`, streamingBehavior: "steer" },
|
|
10988
11263
|
tabId: tabContext.tabId,
|
|
10989
11264
|
}).catch((error) => {
|
|
10990
11265
|
if (isCurrentTabContext(tabContext)) addEvent(`git footer payload refresh failed: ${error.message || String(error)}`, "warn");
|
|
@@ -10998,6 +11273,7 @@ function updateOptionalFeatureAvailability() {
|
|
|
10998
11273
|
optionalFeatureAvailability.gitWorkflow = hasAvailableCommand("git-staged-msg");
|
|
10999
11274
|
optionalFeatureAvailability.releaseNpm = hasAvailableCommand("release-npm");
|
|
11000
11275
|
optionalFeatureAvailability.releaseAur = hasAvailableCommand("release-aur");
|
|
11276
|
+
optionalFeatureAvailability.safetyGuard = hasAvailableCommand("safety-guard") || optionalFeatureAvailability.safetyGuard || statusEntries.has("safety-guard");
|
|
11001
11277
|
optionalFeatureAvailability.statsCommand = hasAvailableCommand("stats");
|
|
11002
11278
|
optionalFeatureAvailability.gitFooterStatus = hasAvailableCommand("git-footer-refresh") || optionalFeatureAvailability.gitFooterStatus || statusEntries.has("git-footer") || statusEntries.has(GIT_FOOTER_WEBUI_STATUS_KEY);
|
|
11003
11279
|
optionalFeatureAvailability.tuiSkillsCommand = hasLoadedRpcCommand("skills");
|
|
@@ -11149,9 +11425,13 @@ function runPublishWorkflow(command) {
|
|
|
11149
11425
|
setPublishMenuOpen(false);
|
|
11150
11426
|
setAppRunnerMenuOpen(false);
|
|
11151
11427
|
setOptionsMenuOpen(false);
|
|
11152
|
-
const
|
|
11428
|
+
const commandText = String(command || "");
|
|
11429
|
+
const commandWithoutSlash = commandText.replace(/^\//, "");
|
|
11430
|
+
const commandName = commandWithoutSlash.split(/\s+/)[0];
|
|
11431
|
+
const commandRest = commandWithoutSlash.slice(commandName.length);
|
|
11153
11432
|
const featureId = OPTIONAL_COMMAND_FEATURES.get(commandName);
|
|
11154
|
-
|
|
11433
|
+
const resolvedCommandName = resolveAvailableCommandName(commandName, { rpcOnly: true });
|
|
11434
|
+
if ((featureId && !isOptionalFeatureEnabled(featureId)) || !resolvedCommandName) {
|
|
11155
11435
|
const tabContext = activeTabContext();
|
|
11156
11436
|
addEvent(commandUnavailableMessage(commandName), "warn");
|
|
11157
11437
|
refreshCommands(tabContext).catch((error) => {
|
|
@@ -11159,7 +11439,7 @@ function runPublishWorkflow(command) {
|
|
|
11159
11439
|
});
|
|
11160
11440
|
return;
|
|
11161
11441
|
}
|
|
11162
|
-
sendPrompt("prompt",
|
|
11442
|
+
sendPrompt("prompt", `/${resolvedCommandName}${commandRest}`);
|
|
11163
11443
|
}
|
|
11164
11444
|
|
|
11165
11445
|
async function runNativeCommandMenu(command) {
|
|
@@ -11687,9 +11967,69 @@ function openNativeNameDialog() {
|
|
|
11687
11967
|
}
|
|
11688
11968
|
|
|
11689
11969
|
async function openNativeResumeSelector(scope = "current") {
|
|
11690
|
-
openNativeCommandDialog({ title: "/resume", message: "
|
|
11970
|
+
openNativeCommandDialog({ title: "/resume", message: "Select a session, then resume, rename metadata, or delete it.", searchPlaceholder: "Filter sessions…" });
|
|
11691
11971
|
renderNativeLoading("Loading sessions…");
|
|
11692
11972
|
const selectedScope = scope === "all" ? "all" : "current";
|
|
11973
|
+
let selectedItem = null;
|
|
11974
|
+
let resumeDeleteArmed = false;
|
|
11975
|
+
|
|
11976
|
+
const renderActions = () => {
|
|
11977
|
+
elements.nativeCommandActions.replaceChildren();
|
|
11978
|
+
const resumeButton = addNativeCommandAction("Resume", async () => {
|
|
11979
|
+
if (!selectedItem || selectedItem.disabled) return;
|
|
11980
|
+
setNativeCommandError("");
|
|
11981
|
+
try {
|
|
11982
|
+
const result = await nativeCommandApi("/api/switch-session", { method: "POST", body: { sessionPath: selectedItem.session.path } });
|
|
11983
|
+
applyResponseTab(result);
|
|
11984
|
+
addTransientMessage({ role: "native", title: "/resume", content: result.data?.message || "Resumed selected session.", level: "info" });
|
|
11985
|
+
closeNativeCommandDialog();
|
|
11986
|
+
await refreshAll();
|
|
11987
|
+
} catch (error) {
|
|
11988
|
+
setNativeCommandError(error.message || String(error));
|
|
11989
|
+
}
|
|
11990
|
+
}, selectedItem && !selectedItem.disabled ? "primary" : undefined);
|
|
11991
|
+
resumeButton.disabled = !selectedItem || selectedItem.disabled;
|
|
11992
|
+
|
|
11993
|
+
const renameButton = addNativeCommandAction("Rename", async () => {
|
|
11994
|
+
if (!selectedItem) return;
|
|
11995
|
+
const nextName = window.prompt("Session display name", selectedItem.session.name || selectedItem.label || "");
|
|
11996
|
+
if (nextName === null) return;
|
|
11997
|
+
setNativeCommandError("");
|
|
11998
|
+
try {
|
|
11999
|
+
const result = await nativeCommandApi("/api/session-rename", { method: "POST", body: { sessionPath: selectedItem.session.path, name: nextName } });
|
|
12000
|
+
addTransientMessage({ role: "native", title: "/resume", content: result.data?.message || "Renamed session metadata.", level: "info" });
|
|
12001
|
+
await openNativeResumeSelector(selectedScope);
|
|
12002
|
+
} catch (error) {
|
|
12003
|
+
setNativeCommandError(error.message || String(error));
|
|
12004
|
+
}
|
|
12005
|
+
});
|
|
12006
|
+
renameButton.disabled = !selectedItem;
|
|
12007
|
+
|
|
12008
|
+
const deleteButton = addNativeCommandAction(resumeDeleteArmed ? "Confirm delete" : "Delete", async () => {
|
|
12009
|
+
if (!selectedItem || selectedItem.disabled) return;
|
|
12010
|
+
if (!resumeDeleteArmed) {
|
|
12011
|
+
resumeDeleteArmed = true;
|
|
12012
|
+
setNativeCommandError("Delete is permanent when trash is unavailable. Click Confirm delete to proceed.");
|
|
12013
|
+
renderActions();
|
|
12014
|
+
return;
|
|
12015
|
+
}
|
|
12016
|
+
setNativeCommandError("");
|
|
12017
|
+
try {
|
|
12018
|
+
const result = await nativeCommandApi("/api/session-delete", { method: "POST", body: { sessionPath: selectedItem.session.path, confirmed: true } });
|
|
12019
|
+
addTransientMessage({ role: "native", title: "/resume", content: result.data?.message || "Session deleted.", level: "warn" });
|
|
12020
|
+
await openNativeResumeSelector(selectedScope);
|
|
12021
|
+
} catch (error) {
|
|
12022
|
+
setNativeCommandError(error.message || String(error));
|
|
12023
|
+
resumeDeleteArmed = false;
|
|
12024
|
+
renderActions();
|
|
12025
|
+
}
|
|
12026
|
+
}, resumeDeleteArmed ? "danger" : undefined);
|
|
12027
|
+
deleteButton.disabled = !selectedItem || selectedItem.disabled;
|
|
12028
|
+
|
|
12029
|
+
addNativeCommandAction(selectedScope === "all" ? "Current cwd" : "All sessions", () => openNativeResumeSelector(selectedScope === "all" ? "current" : "all"));
|
|
12030
|
+
addNativeCommandAction("Cancel", closeNativeCommandDialog);
|
|
12031
|
+
};
|
|
12032
|
+
|
|
11693
12033
|
try {
|
|
11694
12034
|
const response = await nativeCommandApi(`/api/sessions?scope=${encodeURIComponent(selectedScope)}`);
|
|
11695
12035
|
const items = (response.data?.sessions || []).map((session) => ({
|
|
@@ -11701,25 +12041,26 @@ async function openNativeResumeSelector(scope = "current") {
|
|
|
11701
12041
|
disabled: session.current,
|
|
11702
12042
|
session,
|
|
11703
12043
|
}));
|
|
11704
|
-
const render = () =>
|
|
11705
|
-
|
|
11706
|
-
|
|
11707
|
-
|
|
11708
|
-
|
|
11709
|
-
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
|
|
11713
|
-
|
|
11714
|
-
}
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
|
|
11722
|
-
|
|
12044
|
+
const render = () => {
|
|
12045
|
+
resumeDeleteArmed = false;
|
|
12046
|
+
renderNativeSelectorItems(items, {
|
|
12047
|
+
emptyText: selectedScope === "all" ? "No sessions match this filter." : "No sessions for this working directory match this filter.",
|
|
12048
|
+
activeId: selectedItem?.id,
|
|
12049
|
+
onSelect: (item) => {
|
|
12050
|
+
selectedItem = item;
|
|
12051
|
+
setNativeCommandError("");
|
|
12052
|
+
render();
|
|
12053
|
+
renderActions();
|
|
12054
|
+
},
|
|
12055
|
+
});
|
|
12056
|
+
};
|
|
12057
|
+
elements.nativeCommandSearch.oninput = () => {
|
|
12058
|
+
selectedItem = null;
|
|
12059
|
+
resumeDeleteArmed = false;
|
|
12060
|
+
render();
|
|
12061
|
+
renderActions();
|
|
12062
|
+
};
|
|
12063
|
+
renderActions();
|
|
11723
12064
|
render();
|
|
11724
12065
|
} catch (error) {
|
|
11725
12066
|
setNativeCommandError(error.message || String(error));
|
|
@@ -11958,14 +12299,63 @@ async function openNativeSkillsSelector() {
|
|
|
11958
12299
|
}
|
|
11959
12300
|
}
|
|
11960
12301
|
|
|
11961
|
-
function
|
|
12302
|
+
async function openNativeAuthSelector(mode) {
|
|
11962
12303
|
const command = mode === "logout" ? "/logout" : "/login";
|
|
11963
|
-
openNativeCommandDialog({
|
|
11964
|
-
|
|
11965
|
-
"
|
|
11966
|
-
"
|
|
11967
|
-
|
|
11968
|
-
|
|
12304
|
+
openNativeCommandDialog({
|
|
12305
|
+
title: command,
|
|
12306
|
+
message: mode === "logout" ? "Remove stored provider credentials from auth.json." : "Provider login still requires the Pi TUI.",
|
|
12307
|
+
searchPlaceholder: "Filter providers…",
|
|
12308
|
+
});
|
|
12309
|
+
renderNativeLoading("Loading provider auth status…");
|
|
12310
|
+
try {
|
|
12311
|
+
const response = await nativeCommandApi("/api/auth-providers");
|
|
12312
|
+
const providers = mode === "logout" ? response.data?.logoutProviders || [] : response.data?.loginProviders || [];
|
|
12313
|
+
const guidance = String(response.data?.guidance || "").trim();
|
|
12314
|
+
const items = providers.map((provider) => ({
|
|
12315
|
+
id: provider.id,
|
|
12316
|
+
label: provider.name || provider.id,
|
|
12317
|
+
description: provider.authType === "oauth" ? "OAuth / subscription" : "API key",
|
|
12318
|
+
meta: provider.status?.configured
|
|
12319
|
+
? `configured via ${provider.status.source || "stored"}`
|
|
12320
|
+
: "not configured in auth.json",
|
|
12321
|
+
badge: provider.status?.configured ? "configured" : "",
|
|
12322
|
+
provider,
|
|
12323
|
+
}));
|
|
12324
|
+
if (!items.length) {
|
|
12325
|
+
elements.nativeCommandBody.replaceChildren(make("p", "native-command-note", mode === "logout"
|
|
12326
|
+
? "No stored credentials to remove. /logout only removes credentials saved by /login."
|
|
12327
|
+
: "No providers are currently available."));
|
|
12328
|
+
if (guidance) elements.nativeCommandBody.append(make("p", "native-command-note muted", guidance));
|
|
12329
|
+
return;
|
|
12330
|
+
}
|
|
12331
|
+
const render = () => renderNativeSelectorItems(items, {
|
|
12332
|
+
emptyText: "No providers match this filter.",
|
|
12333
|
+
onSelect: async (item) => {
|
|
12334
|
+
if (mode === "login") {
|
|
12335
|
+
setNativeCommandError(`Run /login in the Pi TUI for ${item.label}. Browser login is not implemented yet.`);
|
|
12336
|
+
return;
|
|
12337
|
+
}
|
|
12338
|
+
if (!window.confirm(`Remove stored credentials for ${item.label}?`)) return;
|
|
12339
|
+
setNativeCommandError("");
|
|
12340
|
+
try {
|
|
12341
|
+
const result = await nativeCommandApi("/api/auth-logout", {
|
|
12342
|
+
method: "POST",
|
|
12343
|
+
body: { provider: item.provider.id, confirmed: true },
|
|
12344
|
+
});
|
|
12345
|
+
addTransientMessage({ role: "native", title: "/logout", content: result.data?.message || "Provider credentials removed.", level: "info" });
|
|
12346
|
+
closeNativeCommandDialog();
|
|
12347
|
+
scheduleRefreshState(120);
|
|
12348
|
+
} catch (error) {
|
|
12349
|
+
setNativeCommandError(error.message || String(error));
|
|
12350
|
+
}
|
|
12351
|
+
},
|
|
12352
|
+
});
|
|
12353
|
+
elements.nativeCommandSearch.oninput = render;
|
|
12354
|
+
render();
|
|
12355
|
+
} catch (error) {
|
|
12356
|
+
setNativeCommandError(error.message || String(error));
|
|
12357
|
+
elements.nativeCommandBody.replaceChildren();
|
|
12358
|
+
}
|
|
11969
12359
|
}
|
|
11970
12360
|
|
|
11971
12361
|
async function handleNativeSlashSelectorCommand(message, { usesPromptInput = false } = {}) {
|
|
@@ -12022,7 +12412,7 @@ async function handleNativeSlashSelectorCommand(message, { usesPromptInput = fal
|
|
|
12022
12412
|
return true;
|
|
12023
12413
|
case "login":
|
|
12024
12414
|
case "logout":
|
|
12025
|
-
|
|
12415
|
+
await openNativeAuthSelector(name);
|
|
12026
12416
|
return true;
|
|
12027
12417
|
default:
|
|
12028
12418
|
return false;
|
|
@@ -12139,6 +12529,30 @@ function resetStreamBubble() {
|
|
|
12139
12529
|
streamToolCallSeen = false;
|
|
12140
12530
|
streamThinkingBubble = null;
|
|
12141
12531
|
streamThinking = null;
|
|
12532
|
+
streamMessageActive = false;
|
|
12533
|
+
}
|
|
12534
|
+
|
|
12535
|
+
function liveStreamRenderActive() {
|
|
12536
|
+
return streamMessageActive && currentState?.isStreaming === true && Boolean(streamBubble || streamThinkingBubble || streamRawText);
|
|
12537
|
+
}
|
|
12538
|
+
|
|
12539
|
+
/**
|
|
12540
|
+
* The chat DOM was rebuilt while an assistant message is still streaming:
|
|
12541
|
+
* drop references to the detached nodes but keep stream text state, then
|
|
12542
|
+
* re-append the live thinking/text bubbles so no partial output is lost.
|
|
12543
|
+
*/
|
|
12544
|
+
function restoreStreamRenderAfterChatRebuild() {
|
|
12545
|
+
const thinkingText = streamThinking?.textContent || "";
|
|
12546
|
+
const thinkingComplete = streamThinkingBubble?.classList.contains("complete") === true;
|
|
12547
|
+
streamBubble = null;
|
|
12548
|
+
streamText = null;
|
|
12549
|
+
streamThinkingBubble = null;
|
|
12550
|
+
streamThinking = null;
|
|
12551
|
+
streamBubbleVisibleSince = 0;
|
|
12552
|
+
if (thinkingText && setStreamingThinkingText(thinkingText) && thinkingComplete) {
|
|
12553
|
+
streamThinkingBubble?.classList.add("complete");
|
|
12554
|
+
}
|
|
12555
|
+
if (stripTodoProgressLines(streamRawText, { streaming: true })) renderStreamingAssistantText();
|
|
12142
12556
|
}
|
|
12143
12557
|
|
|
12144
12558
|
function thinkingDeltaText(update) {
|
|
@@ -12323,11 +12737,12 @@ function renderNetworkStatus() {
|
|
|
12323
12737
|
const list = make("div", "network-url-list");
|
|
12324
12738
|
|
|
12325
12739
|
const addUrl = (label, url) => {
|
|
12326
|
-
|
|
12740
|
+
const href = safeHttpUrl(url);
|
|
12741
|
+
if (!href) return;
|
|
12327
12742
|
const row = make("div", "network-status-url-row");
|
|
12328
12743
|
const labelNode = make("span", "network-status-url-label", label);
|
|
12329
12744
|
const link = make("a", "network-status-url", url);
|
|
12330
|
-
link.href =
|
|
12745
|
+
link.href = href;
|
|
12331
12746
|
link.target = "_blank";
|
|
12332
12747
|
link.rel = "noreferrer";
|
|
12333
12748
|
row.append(labelNode, link);
|
|
@@ -12366,8 +12781,10 @@ async function refreshMessages(tabContext = activeTabContext()) {
|
|
|
12366
12781
|
const response = await api("/api/messages", { tabId: tabContext.tabId });
|
|
12367
12782
|
if (!isCurrentTabContext(tabContext)) return;
|
|
12368
12783
|
latestMessages = response.data?.messages || [];
|
|
12369
|
-
|
|
12784
|
+
const preserveLiveStream = liveStreamRenderActive();
|
|
12785
|
+
if (!preserveLiveStream) resetStreamBubble();
|
|
12370
12786
|
renderMessages(latestMessages);
|
|
12787
|
+
if (preserveLiveStream) restoreStreamRenderAfterChatRebuild();
|
|
12371
12788
|
markTabOutputSeen();
|
|
12372
12789
|
renderFooter();
|
|
12373
12790
|
}
|
|
@@ -12417,23 +12834,67 @@ function syncModelSelectToState() {
|
|
|
12417
12834
|
}
|
|
12418
12835
|
}
|
|
12419
12836
|
|
|
12837
|
+
function normalizedCommandIdentity(command) {
|
|
12838
|
+
return [commandBaseName(command.name), command.description, command.source, command.enabled ? "enabled" : "disabled"].join("\u0000");
|
|
12839
|
+
}
|
|
12840
|
+
|
|
12841
|
+
function combineIdenticalDuplicateCommands(commands) {
|
|
12842
|
+
const duplicateGroups = new Map();
|
|
12843
|
+
for (const command of commands) {
|
|
12844
|
+
if (commandBaseName(command.name) === command.name) continue;
|
|
12845
|
+
const key = normalizedCommandIdentity(command);
|
|
12846
|
+
if (!duplicateGroups.has(key)) duplicateGroups.set(key, []);
|
|
12847
|
+
duplicateGroups.get(key).push(command);
|
|
12848
|
+
}
|
|
12849
|
+
const combinedKeys = new Set([...duplicateGroups.entries()].filter(([, group]) => group.length > 1).map(([key]) => key));
|
|
12850
|
+
const emittedKeys = new Set();
|
|
12851
|
+
const emittedNames = new Set();
|
|
12852
|
+
const result = [];
|
|
12853
|
+
|
|
12854
|
+
for (const command of commands) {
|
|
12855
|
+
const key = normalizedCommandIdentity(command);
|
|
12856
|
+
if (!combinedKeys.has(key)) {
|
|
12857
|
+
if (emittedNames.has(command.name)) continue;
|
|
12858
|
+
emittedNames.add(command.name);
|
|
12859
|
+
result.push(command);
|
|
12860
|
+
continue;
|
|
12861
|
+
}
|
|
12862
|
+
|
|
12863
|
+
if (emittedKeys.has(key)) continue;
|
|
12864
|
+
emittedKeys.add(key);
|
|
12865
|
+
const group = duplicateGroups.get(key);
|
|
12866
|
+
const baseName = commandBaseName(command.name);
|
|
12867
|
+
const displayName = emittedNames.has(baseName) ? command.name : baseName;
|
|
12868
|
+
emittedNames.add(displayName);
|
|
12869
|
+
result.push({
|
|
12870
|
+
...command,
|
|
12871
|
+
name: displayName,
|
|
12872
|
+
invokeName: command.name,
|
|
12873
|
+
duplicateNames: group.map((item) => item.name),
|
|
12874
|
+
duplicateCount: group.length,
|
|
12875
|
+
location: command.location || `${group.length} identical loaded commands`,
|
|
12876
|
+
});
|
|
12877
|
+
}
|
|
12878
|
+
|
|
12879
|
+
return result;
|
|
12880
|
+
}
|
|
12881
|
+
|
|
12420
12882
|
function normalizeCommands(commands, { dedupe = true } = {}) {
|
|
12421
|
-
const
|
|
12422
|
-
|
|
12423
|
-
|
|
12424
|
-
|
|
12425
|
-
|
|
12426
|
-
|
|
12427
|
-
|
|
12428
|
-
|
|
12429
|
-
|
|
12430
|
-
|
|
12431
|
-
|
|
12432
|
-
if (!dedupe) return true;
|
|
12433
|
-
if (seen.has(command.name)) return false;
|
|
12434
|
-
seen.add(command.name);
|
|
12435
|
-
return true;
|
|
12883
|
+
const normalized = (commands || [])
|
|
12884
|
+
.map((command) => {
|
|
12885
|
+
const name = String(command.name || "").trim();
|
|
12886
|
+
return {
|
|
12887
|
+
name,
|
|
12888
|
+
invokeName: name,
|
|
12889
|
+
description: String(command.description || "").trim(),
|
|
12890
|
+
source: String(command.source || "command").trim(),
|
|
12891
|
+
location: String(command.location || "").trim(),
|
|
12892
|
+
enabled: command.enabled !== false,
|
|
12893
|
+
};
|
|
12436
12894
|
})
|
|
12895
|
+
.filter((command) => command.name);
|
|
12896
|
+
|
|
12897
|
+
return (dedupe ? combineIdenticalDuplicateCommands(normalized) : normalized)
|
|
12437
12898
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
12438
12899
|
}
|
|
12439
12900
|
|
|
@@ -12500,11 +12961,13 @@ function getPathTrigger() {
|
|
|
12500
12961
|
function scoreCommandSuggestion(command, query) {
|
|
12501
12962
|
if (!query) return 0;
|
|
12502
12963
|
const q = query.toLowerCase();
|
|
12503
|
-
const
|
|
12964
|
+
const names = [command.name, command.invokeName, ...(command.duplicateNames || [])]
|
|
12965
|
+
.filter(Boolean)
|
|
12966
|
+
.map((name) => name.toLowerCase());
|
|
12504
12967
|
const description = command.description.toLowerCase();
|
|
12505
|
-
if (name === q) return 0;
|
|
12506
|
-
if (name.startsWith(q)) return 1;
|
|
12507
|
-
if (name.includes(q)) return 2;
|
|
12968
|
+
if (names.some((name) => name === q)) return 0;
|
|
12969
|
+
if (names.some((name) => name.startsWith(q))) return 1;
|
|
12970
|
+
if (names.some((name) => name.includes(q))) return 2;
|
|
12508
12971
|
if (description.includes(q)) return 3;
|
|
12509
12972
|
return Number.POSITIVE_INFINITY;
|
|
12510
12973
|
}
|
|
@@ -12524,7 +12987,7 @@ function commandSearchQuery() {
|
|
|
12524
12987
|
|
|
12525
12988
|
function commandMatchesSearch(command, query) {
|
|
12526
12989
|
if (!query) return true;
|
|
12527
|
-
return [command.name, command.description, command.source, command.location]
|
|
12990
|
+
return [command.name, command.invokeName, ...(command.duplicateNames || []), command.description, command.source, command.location]
|
|
12528
12991
|
.filter(Boolean)
|
|
12529
12992
|
.join(" ")
|
|
12530
12993
|
.toLowerCase()
|
|
@@ -13227,6 +13690,9 @@ async function runUserBashCommand(parsed, { usesPromptInput = false, targetTabId
|
|
|
13227
13690
|
const result = response.data || {};
|
|
13228
13691
|
applyResponseTab(response);
|
|
13229
13692
|
if (isCurrentTabContext(tabContext)) {
|
|
13693
|
+
for (const warning of response.warnings || []) {
|
|
13694
|
+
if (warning) addEvent(String(warning), "warn");
|
|
13695
|
+
}
|
|
13230
13696
|
addTransientMessage({
|
|
13231
13697
|
role: "bashExecution",
|
|
13232
13698
|
title: excludeFromContext ? "bash (!! complete)" : "bash (! complete)",
|
|
@@ -13307,6 +13773,7 @@ async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = acti
|
|
|
13307
13773
|
try {
|
|
13308
13774
|
const prepared = attachments.length ? await prepareAttachmentsForPrompt(attachments, targetTabId) : { images: [], uploadedFiles: [], inlineImageIds: new Set() };
|
|
13309
13775
|
message = composeMessageWithAttachments(originalMessage, prepared.uploadedFiles, prepared.inlineImageIds);
|
|
13776
|
+
if (kind === "prompt" && attachments.length === 0) message = resolveRpcSlashCommandMessage(message);
|
|
13310
13777
|
const bodyBase = { message };
|
|
13311
13778
|
if (prepared.images.length) bodyBase.images = prepared.images;
|
|
13312
13779
|
if (!message.startsWith("/")) {
|
|
@@ -13336,19 +13803,8 @@ async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = acti
|
|
|
13336
13803
|
} else if (targetStillActive && kind === "follow-up" && currentState?.isStreaming) {
|
|
13337
13804
|
setRunIndicatorActivity("Follow-up queued; current agent run is still active…");
|
|
13338
13805
|
}
|
|
13339
|
-
if (targetStillActive && response?.command === "native_slash_command"
|
|
13340
|
-
|
|
13341
|
-
await copyText(response.data.copyText);
|
|
13342
|
-
} catch (error) {
|
|
13343
|
-
response.data.message = `${response.data.message || "Copy requested, but clipboard access failed."}\n\nClipboard access failed: ${error.message}\n\n${response.data.copyText}`;
|
|
13344
|
-
response.data.level = "warn";
|
|
13345
|
-
}
|
|
13346
|
-
}
|
|
13347
|
-
if (targetStillActive && response?.command === "native_slash_command" && response.data?.download) {
|
|
13348
|
-
if (triggerNativeDownload(response.data.download)) addEvent(`download started: ${response.data.download.fileName || response.data.download.url}`, "info");
|
|
13349
|
-
}
|
|
13350
|
-
if (targetStillActive && response?.command === "native_slash_command" && response.data?.message) {
|
|
13351
|
-
addTransientMessage({ role: "native", title: message.split(/\s+/, 1)[0], content: response.data.message, level: response.data.level || "info" });
|
|
13806
|
+
if (targetStillActive && response?.command === "native_slash_command") {
|
|
13807
|
+
applyNativeSlashCommandEffects(response, message, tabContext);
|
|
13352
13808
|
}
|
|
13353
13809
|
if (usesPromptInput) {
|
|
13354
13810
|
clearAttachments(targetTabId);
|
|
@@ -13361,8 +13817,8 @@ async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = acti
|
|
|
13361
13817
|
}
|
|
13362
13818
|
if (targetStillActive) {
|
|
13363
13819
|
hideCommandSuggestions();
|
|
13364
|
-
scheduleRefreshState(120, tabContext);
|
|
13365
|
-
} else {
|
|
13820
|
+
if (response?.command !== "native_slash_command") scheduleRefreshState(120, tabContext);
|
|
13821
|
+
} else if (response?.command !== "native_slash_command") {
|
|
13366
13822
|
scheduleRefreshTabs(300);
|
|
13367
13823
|
}
|
|
13368
13824
|
} catch (error) {
|
|
@@ -13661,12 +14117,14 @@ function handleEvent(event) {
|
|
|
13661
14117
|
renderFeedbackTray();
|
|
13662
14118
|
break;
|
|
13663
14119
|
case "agent_end":
|
|
14120
|
+
streamMessageActive = false;
|
|
13664
14121
|
addEvent("agent finished");
|
|
13665
14122
|
notifyAgentDone(event.tabId || activeTabId, { activity: event.tabActivity, tabTitle: event.tabTitle });
|
|
13666
14123
|
clearContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
13667
14124
|
if (currentState) currentState = { ...currentState, isStreaming: false };
|
|
13668
14125
|
clearRunIndicatorActivity();
|
|
13669
14126
|
markTabOutputSeen();
|
|
14127
|
+
requestGitFooterWebuiPayload(tabContext, { force: true });
|
|
13670
14128
|
scheduleRefreshState();
|
|
13671
14129
|
scheduleRefreshMessages();
|
|
13672
14130
|
scheduleRefreshFooter();
|
|
@@ -13687,6 +14145,7 @@ function handleEvent(event) {
|
|
|
13687
14145
|
case "message_start":
|
|
13688
14146
|
if (event.message?.role === "assistant") {
|
|
13689
14147
|
resetStreamBubble();
|
|
14148
|
+
streamMessageActive = true;
|
|
13690
14149
|
setRunIndicatorActivity("Starting assistant message…", { scroll: false });
|
|
13691
14150
|
}
|
|
13692
14151
|
break;
|
|
@@ -13694,6 +14153,7 @@ function handleEvent(event) {
|
|
|
13694
14153
|
handleMessageUpdate(event);
|
|
13695
14154
|
break;
|
|
13696
14155
|
case "message_end":
|
|
14156
|
+
streamMessageActive = false;
|
|
13697
14157
|
if (runIndicatorIsActive()) setRunIndicatorActivity("Assistant message finished; waiting for the next step…", { scroll: false });
|
|
13698
14158
|
scheduleRefreshMessages();
|
|
13699
14159
|
scheduleRefreshState();
|