@firstpick/pi-package-webui 0.3.7 → 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 +233 -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 +547 -93
- 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 +44 -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"],
|
|
@@ -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");
|
|
@@ -6316,7 +6490,7 @@ async function changeActiveTabCwd() {
|
|
|
6316
6490
|
const currentCwd = latestWorkspace?.cwd || tab.cwd || "";
|
|
6317
6491
|
const cwd = await pickCwd(tab, currentCwd);
|
|
6318
6492
|
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;
|
|
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;
|
|
6320
6494
|
|
|
6321
6495
|
saveActiveDraft();
|
|
6322
6496
|
try {
|
|
@@ -6592,7 +6766,7 @@ function renderStatus() {
|
|
|
6592
6766
|
File: state?.sessionFile || "in-memory",
|
|
6593
6767
|
Messages: String(state?.messageCount ?? "?"),
|
|
6594
6768
|
Queue: String(state?.pendingMessageCount ?? 0),
|
|
6595
|
-
"Auto compact": state
|
|
6769
|
+
"Auto compact": footerAutoCompactionEnabled(state) ? "on" : "off",
|
|
6596
6770
|
};
|
|
6597
6771
|
for (const [key, value] of Object.entries(details)) {
|
|
6598
6772
|
elements.stateDetails.append(make("dt", undefined, key), make("dd", undefined, value));
|
|
@@ -6826,8 +7000,9 @@ function appendReleaseNpmTerminalLine(parent, line) {
|
|
|
6826
7000
|
|
|
6827
7001
|
async function sendReleaseNpmCommand(command) {
|
|
6828
7002
|
const tabContext = activeTabContext();
|
|
7003
|
+
const resolvedCommand = resolveRpcSlashCommandMessage(command);
|
|
6829
7004
|
try {
|
|
6830
|
-
await api("/api/prompt", { method: "POST", body: { message:
|
|
7005
|
+
await api("/api/prompt", { method: "POST", body: { message: resolvedCommand }, tabId: tabContext.tabId });
|
|
6831
7006
|
if (!isCurrentTabContext(tabContext)) return;
|
|
6832
7007
|
addEvent(`${command} sent`, "info");
|
|
6833
7008
|
scheduleRefreshState(120, tabContext);
|
|
@@ -10747,6 +10922,59 @@ function renderAllMessages({ preserveScroll = false } = {}) {
|
|
|
10747
10922
|
updateStickyUserPromptButton();
|
|
10748
10923
|
}
|
|
10749
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
|
+
|
|
10750
10978
|
function addTransientMessage({ role = "notice", title, content, level = "info", ...details }) {
|
|
10751
10979
|
transientMessages.push({
|
|
10752
10980
|
role,
|
|
@@ -10908,10 +11136,11 @@ function setOptionsMenuOpen(open) {
|
|
|
10908
11136
|
}
|
|
10909
11137
|
|
|
10910
11138
|
function optionalFeatureIdForCommand(name) {
|
|
10911
|
-
|
|
10912
|
-
if (
|
|
10913
|
-
if (
|
|
10914
|
-
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";
|
|
10915
11144
|
return null;
|
|
10916
11145
|
}
|
|
10917
11146
|
|
|
@@ -10926,12 +11155,51 @@ function visibleCommands() {
|
|
|
10926
11155
|
return availableCommands.filter(isCommandVisible);
|
|
10927
11156
|
}
|
|
10928
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
|
+
|
|
10929
11197
|
function hasAvailableCommand(name) {
|
|
10930
|
-
return
|
|
11198
|
+
return !!resolveAvailableCommand(name);
|
|
10931
11199
|
}
|
|
10932
11200
|
|
|
10933
11201
|
function hasLoadedRpcCommand(name) {
|
|
10934
|
-
return
|
|
11202
|
+
return !!resolveAvailableCommand(name, { rpcOnly: true });
|
|
10935
11203
|
}
|
|
10936
11204
|
|
|
10937
11205
|
function optionalFeatureUnavailableMessage(featureId) {
|
|
@@ -10977,14 +11245,15 @@ function resetOptionalFeatureAvailability() {
|
|
|
10977
11245
|
function requestGitFooterWebuiPayload(tabContext = activeTabContext(), { force = false } = {}) {
|
|
10978
11246
|
if (!tabContext.tabId || isOptionalFeatureDisabled("gitFooterStatus")) return;
|
|
10979
11247
|
if (currentState?.isStreaming || currentState?.isCompacting) return;
|
|
10980
|
-
|
|
11248
|
+
const refreshCommand = resolveAvailableCommandName("git-footer-refresh", { rpcOnly: true });
|
|
11249
|
+
if (!refreshCommand || (!force && statusEntries.has(GIT_FOOTER_WEBUI_STATUS_KEY))) return;
|
|
10981
11250
|
if (gitFooterPayloadRefreshInFlightByTab.has(tabContext.tabId)) return;
|
|
10982
11251
|
|
|
10983
11252
|
gitFooterPayloadRefreshInFlightByTab.add(tabContext.tabId);
|
|
10984
11253
|
if (isCurrentTabContext(tabContext)) renderFooter();
|
|
10985
11254
|
api("/api/prompt", {
|
|
10986
11255
|
method: "POST",
|
|
10987
|
-
body: { message:
|
|
11256
|
+
body: { message: `/${refreshCommand} --webui-silent`, streamingBehavior: "steer" },
|
|
10988
11257
|
tabId: tabContext.tabId,
|
|
10989
11258
|
}).catch((error) => {
|
|
10990
11259
|
if (isCurrentTabContext(tabContext)) addEvent(`git footer payload refresh failed: ${error.message || String(error)}`, "warn");
|
|
@@ -10998,6 +11267,7 @@ function updateOptionalFeatureAvailability() {
|
|
|
10998
11267
|
optionalFeatureAvailability.gitWorkflow = hasAvailableCommand("git-staged-msg");
|
|
10999
11268
|
optionalFeatureAvailability.releaseNpm = hasAvailableCommand("release-npm");
|
|
11000
11269
|
optionalFeatureAvailability.releaseAur = hasAvailableCommand("release-aur");
|
|
11270
|
+
optionalFeatureAvailability.safetyGuard = hasAvailableCommand("safety-guard") || optionalFeatureAvailability.safetyGuard || statusEntries.has("safety-guard");
|
|
11001
11271
|
optionalFeatureAvailability.statsCommand = hasAvailableCommand("stats");
|
|
11002
11272
|
optionalFeatureAvailability.gitFooterStatus = hasAvailableCommand("git-footer-refresh") || optionalFeatureAvailability.gitFooterStatus || statusEntries.has("git-footer") || statusEntries.has(GIT_FOOTER_WEBUI_STATUS_KEY);
|
|
11003
11273
|
optionalFeatureAvailability.tuiSkillsCommand = hasLoadedRpcCommand("skills");
|
|
@@ -11149,9 +11419,13 @@ function runPublishWorkflow(command) {
|
|
|
11149
11419
|
setPublishMenuOpen(false);
|
|
11150
11420
|
setAppRunnerMenuOpen(false);
|
|
11151
11421
|
setOptionsMenuOpen(false);
|
|
11152
|
-
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);
|
|
11153
11426
|
const featureId = OPTIONAL_COMMAND_FEATURES.get(commandName);
|
|
11154
|
-
|
|
11427
|
+
const resolvedCommandName = resolveAvailableCommandName(commandName, { rpcOnly: true });
|
|
11428
|
+
if ((featureId && !isOptionalFeatureEnabled(featureId)) || !resolvedCommandName) {
|
|
11155
11429
|
const tabContext = activeTabContext();
|
|
11156
11430
|
addEvent(commandUnavailableMessage(commandName), "warn");
|
|
11157
11431
|
refreshCommands(tabContext).catch((error) => {
|
|
@@ -11159,7 +11433,7 @@ function runPublishWorkflow(command) {
|
|
|
11159
11433
|
});
|
|
11160
11434
|
return;
|
|
11161
11435
|
}
|
|
11162
|
-
sendPrompt("prompt",
|
|
11436
|
+
sendPrompt("prompt", `/${resolvedCommandName}${commandRest}`);
|
|
11163
11437
|
}
|
|
11164
11438
|
|
|
11165
11439
|
async function runNativeCommandMenu(command) {
|
|
@@ -11687,9 +11961,69 @@ function openNativeNameDialog() {
|
|
|
11687
11961
|
}
|
|
11688
11962
|
|
|
11689
11963
|
async function openNativeResumeSelector(scope = "current") {
|
|
11690
|
-
openNativeCommandDialog({ title: "/resume", message: "
|
|
11964
|
+
openNativeCommandDialog({ title: "/resume", message: "Select a session, then resume, rename metadata, or delete it.", searchPlaceholder: "Filter sessions…" });
|
|
11691
11965
|
renderNativeLoading("Loading sessions…");
|
|
11692
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
|
+
|
|
11693
12027
|
try {
|
|
11694
12028
|
const response = await nativeCommandApi(`/api/sessions?scope=${encodeURIComponent(selectedScope)}`);
|
|
11695
12029
|
const items = (response.data?.sessions || []).map((session) => ({
|
|
@@ -11701,25 +12035,26 @@ async function openNativeResumeSelector(scope = "current") {
|
|
|
11701
12035
|
disabled: session.current,
|
|
11702
12036
|
session,
|
|
11703
12037
|
}));
|
|
11704
|
-
const render = () =>
|
|
11705
|
-
|
|
11706
|
-
|
|
11707
|
-
|
|
11708
|
-
|
|
11709
|
-
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
|
|
11713
|
-
|
|
11714
|
-
}
|
|
11715
|
-
|
|
11716
|
-
|
|
11717
|
-
|
|
11718
|
-
|
|
11719
|
-
|
|
11720
|
-
|
|
11721
|
-
|
|
11722
|
-
|
|
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();
|
|
11723
12058
|
render();
|
|
11724
12059
|
} catch (error) {
|
|
11725
12060
|
setNativeCommandError(error.message || String(error));
|
|
@@ -11958,14 +12293,63 @@ async function openNativeSkillsSelector() {
|
|
|
11958
12293
|
}
|
|
11959
12294
|
}
|
|
11960
12295
|
|
|
11961
|
-
function
|
|
12296
|
+
async function openNativeAuthSelector(mode) {
|
|
11962
12297
|
const command = mode === "logout" ? "/logout" : "/login";
|
|
11963
|
-
openNativeCommandDialog({
|
|
11964
|
-
|
|
11965
|
-
"
|
|
11966
|
-
"
|
|
11967
|
-
|
|
11968
|
-
|
|
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
|
+
}
|
|
11969
12353
|
}
|
|
11970
12354
|
|
|
11971
12355
|
async function handleNativeSlashSelectorCommand(message, { usesPromptInput = false } = {}) {
|
|
@@ -12022,7 +12406,7 @@ async function handleNativeSlashSelectorCommand(message, { usesPromptInput = fal
|
|
|
12022
12406
|
return true;
|
|
12023
12407
|
case "login":
|
|
12024
12408
|
case "logout":
|
|
12025
|
-
|
|
12409
|
+
await openNativeAuthSelector(name);
|
|
12026
12410
|
return true;
|
|
12027
12411
|
default:
|
|
12028
12412
|
return false;
|
|
@@ -12139,6 +12523,30 @@ function resetStreamBubble() {
|
|
|
12139
12523
|
streamToolCallSeen = false;
|
|
12140
12524
|
streamThinkingBubble = null;
|
|
12141
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();
|
|
12142
12550
|
}
|
|
12143
12551
|
|
|
12144
12552
|
function thinkingDeltaText(update) {
|
|
@@ -12323,11 +12731,12 @@ function renderNetworkStatus() {
|
|
|
12323
12731
|
const list = make("div", "network-url-list");
|
|
12324
12732
|
|
|
12325
12733
|
const addUrl = (label, url) => {
|
|
12326
|
-
|
|
12734
|
+
const href = safeHttpUrl(url);
|
|
12735
|
+
if (!href) return;
|
|
12327
12736
|
const row = make("div", "network-status-url-row");
|
|
12328
12737
|
const labelNode = make("span", "network-status-url-label", label);
|
|
12329
12738
|
const link = make("a", "network-status-url", url);
|
|
12330
|
-
link.href =
|
|
12739
|
+
link.href = href;
|
|
12331
12740
|
link.target = "_blank";
|
|
12332
12741
|
link.rel = "noreferrer";
|
|
12333
12742
|
row.append(labelNode, link);
|
|
@@ -12366,8 +12775,10 @@ async function refreshMessages(tabContext = activeTabContext()) {
|
|
|
12366
12775
|
const response = await api("/api/messages", { tabId: tabContext.tabId });
|
|
12367
12776
|
if (!isCurrentTabContext(tabContext)) return;
|
|
12368
12777
|
latestMessages = response.data?.messages || [];
|
|
12369
|
-
|
|
12778
|
+
const preserveLiveStream = liveStreamRenderActive();
|
|
12779
|
+
if (!preserveLiveStream) resetStreamBubble();
|
|
12370
12780
|
renderMessages(latestMessages);
|
|
12781
|
+
if (preserveLiveStream) restoreStreamRenderAfterChatRebuild();
|
|
12371
12782
|
markTabOutputSeen();
|
|
12372
12783
|
renderFooter();
|
|
12373
12784
|
}
|
|
@@ -12417,23 +12828,67 @@ function syncModelSelectToState() {
|
|
|
12417
12828
|
}
|
|
12418
12829
|
}
|
|
12419
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
|
+
|
|
12420
12876
|
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;
|
|
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
|
+
};
|
|
12436
12888
|
})
|
|
12889
|
+
.filter((command) => command.name);
|
|
12890
|
+
|
|
12891
|
+
return (dedupe ? combineIdenticalDuplicateCommands(normalized) : normalized)
|
|
12437
12892
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
12438
12893
|
}
|
|
12439
12894
|
|
|
@@ -12500,11 +12955,13 @@ function getPathTrigger() {
|
|
|
12500
12955
|
function scoreCommandSuggestion(command, query) {
|
|
12501
12956
|
if (!query) return 0;
|
|
12502
12957
|
const q = query.toLowerCase();
|
|
12503
|
-
const
|
|
12958
|
+
const names = [command.name, command.invokeName, ...(command.duplicateNames || [])]
|
|
12959
|
+
.filter(Boolean)
|
|
12960
|
+
.map((name) => name.toLowerCase());
|
|
12504
12961
|
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;
|
|
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;
|
|
12508
12965
|
if (description.includes(q)) return 3;
|
|
12509
12966
|
return Number.POSITIVE_INFINITY;
|
|
12510
12967
|
}
|
|
@@ -12524,7 +12981,7 @@ function commandSearchQuery() {
|
|
|
12524
12981
|
|
|
12525
12982
|
function commandMatchesSearch(command, query) {
|
|
12526
12983
|
if (!query) return true;
|
|
12527
|
-
return [command.name, command.description, command.source, command.location]
|
|
12984
|
+
return [command.name, command.invokeName, ...(command.duplicateNames || []), command.description, command.source, command.location]
|
|
12528
12985
|
.filter(Boolean)
|
|
12529
12986
|
.join(" ")
|
|
12530
12987
|
.toLowerCase()
|
|
@@ -13227,6 +13684,9 @@ async function runUserBashCommand(parsed, { usesPromptInput = false, targetTabId
|
|
|
13227
13684
|
const result = response.data || {};
|
|
13228
13685
|
applyResponseTab(response);
|
|
13229
13686
|
if (isCurrentTabContext(tabContext)) {
|
|
13687
|
+
for (const warning of response.warnings || []) {
|
|
13688
|
+
if (warning) addEvent(String(warning), "warn");
|
|
13689
|
+
}
|
|
13230
13690
|
addTransientMessage({
|
|
13231
13691
|
role: "bashExecution",
|
|
13232
13692
|
title: excludeFromContext ? "bash (!! complete)" : "bash (! complete)",
|
|
@@ -13307,6 +13767,7 @@ async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = acti
|
|
|
13307
13767
|
try {
|
|
13308
13768
|
const prepared = attachments.length ? await prepareAttachmentsForPrompt(attachments, targetTabId) : { images: [], uploadedFiles: [], inlineImageIds: new Set() };
|
|
13309
13769
|
message = composeMessageWithAttachments(originalMessage, prepared.uploadedFiles, prepared.inlineImageIds);
|
|
13770
|
+
if (kind === "prompt" && attachments.length === 0) message = resolveRpcSlashCommandMessage(message);
|
|
13310
13771
|
const bodyBase = { message };
|
|
13311
13772
|
if (prepared.images.length) bodyBase.images = prepared.images;
|
|
13312
13773
|
if (!message.startsWith("/")) {
|
|
@@ -13336,19 +13797,8 @@ async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = acti
|
|
|
13336
13797
|
} else if (targetStillActive && kind === "follow-up" && currentState?.isStreaming) {
|
|
13337
13798
|
setRunIndicatorActivity("Follow-up queued; current agent run is still active…");
|
|
13338
13799
|
}
|
|
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" });
|
|
13800
|
+
if (targetStillActive && response?.command === "native_slash_command") {
|
|
13801
|
+
applyNativeSlashCommandEffects(response, message, tabContext);
|
|
13352
13802
|
}
|
|
13353
13803
|
if (usesPromptInput) {
|
|
13354
13804
|
clearAttachments(targetTabId);
|
|
@@ -13361,8 +13811,8 @@ async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = acti
|
|
|
13361
13811
|
}
|
|
13362
13812
|
if (targetStillActive) {
|
|
13363
13813
|
hideCommandSuggestions();
|
|
13364
|
-
scheduleRefreshState(120, tabContext);
|
|
13365
|
-
} else {
|
|
13814
|
+
if (response?.command !== "native_slash_command") scheduleRefreshState(120, tabContext);
|
|
13815
|
+
} else if (response?.command !== "native_slash_command") {
|
|
13366
13816
|
scheduleRefreshTabs(300);
|
|
13367
13817
|
}
|
|
13368
13818
|
} catch (error) {
|
|
@@ -13661,12 +14111,14 @@ function handleEvent(event) {
|
|
|
13661
14111
|
renderFeedbackTray();
|
|
13662
14112
|
break;
|
|
13663
14113
|
case "agent_end":
|
|
14114
|
+
streamMessageActive = false;
|
|
13664
14115
|
addEvent("agent finished");
|
|
13665
14116
|
notifyAgentDone(event.tabId || activeTabId, { activity: event.tabActivity, tabTitle: event.tabTitle });
|
|
13666
14117
|
clearContextUsageUnknownAfterCompaction(event.tabId || activeTabId);
|
|
13667
14118
|
if (currentState) currentState = { ...currentState, isStreaming: false };
|
|
13668
14119
|
clearRunIndicatorActivity();
|
|
13669
14120
|
markTabOutputSeen();
|
|
14121
|
+
requestGitFooterWebuiPayload(tabContext, { force: true });
|
|
13670
14122
|
scheduleRefreshState();
|
|
13671
14123
|
scheduleRefreshMessages();
|
|
13672
14124
|
scheduleRefreshFooter();
|
|
@@ -13687,6 +14139,7 @@ function handleEvent(event) {
|
|
|
13687
14139
|
case "message_start":
|
|
13688
14140
|
if (event.message?.role === "assistant") {
|
|
13689
14141
|
resetStreamBubble();
|
|
14142
|
+
streamMessageActive = true;
|
|
13690
14143
|
setRunIndicatorActivity("Starting assistant message…", { scroll: false });
|
|
13691
14144
|
}
|
|
13692
14145
|
break;
|
|
@@ -13694,6 +14147,7 @@ function handleEvent(event) {
|
|
|
13694
14147
|
handleMessageUpdate(event);
|
|
13695
14148
|
break;
|
|
13696
14149
|
case "message_end":
|
|
14150
|
+
streamMessageActive = false;
|
|
13697
14151
|
if (runIndicatorIsActive()) setRunIndicatorActivity("Assistant message finished; waiting for the next step…", { scroll: false });
|
|
13698
14152
|
scheduleRefreshMessages();
|
|
13699
14153
|
scheduleRefreshState();
|