@firstpick/pi-package-webui 0.2.8 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/pi-webui.mjs +38 -1
- package/package.json +6 -1
- package/public/app.js +778 -59
- package/public/index.html +51 -10
- package/public/styles.css +457 -17
- package/tests/mobile-static.test.mjs +60 -4
package/public/app.js
CHANGED
|
@@ -32,7 +32,6 @@ const elements = {
|
|
|
32
32
|
attachmentTray: $("#attachmentTray"),
|
|
33
33
|
attachButton: $("#attachButton"),
|
|
34
34
|
attachmentInput: $("#attachmentInput"),
|
|
35
|
-
busyBehavior: $("#busyBehavior"),
|
|
36
35
|
steerButton: $("#steerButton"),
|
|
37
36
|
followUpButton: $("#followUpButton"),
|
|
38
37
|
abortButton: $("#abortButton"),
|
|
@@ -91,6 +90,25 @@ const elements = {
|
|
|
91
90
|
sidePanel: $("#sidePanel"),
|
|
92
91
|
stateDetails: $("#stateDetails"),
|
|
93
92
|
queueBox: $("#queueBox"),
|
|
93
|
+
queueCountBadge: $("#queueCountBadge"),
|
|
94
|
+
createPromptListButton: $("#createPromptListButton"),
|
|
95
|
+
loadPromptListButton: $("#loadPromptListButton"),
|
|
96
|
+
runLoadedPromptListButton: $("#runLoadedPromptListButton"),
|
|
97
|
+
loadedPromptListBox: $("#loadedPromptListBox"),
|
|
98
|
+
promptListDialog: $("#promptListDialog"),
|
|
99
|
+
promptListDialogTitle: $("#promptListDialogTitle"),
|
|
100
|
+
promptListNameInput: $("#promptListNameInput"),
|
|
101
|
+
promptListEditorRows: $("#promptListEditorRows"),
|
|
102
|
+
promptListAddPromptButton: $("#promptListAddPromptButton"),
|
|
103
|
+
promptListLoadPanel: $("#promptListLoadPanel"),
|
|
104
|
+
promptListSelect: $("#promptListSelect"),
|
|
105
|
+
promptListLoadSelectedButton: $("#promptListLoadSelectedButton"),
|
|
106
|
+
promptListDeleteSelectedButton: $("#promptListDeleteSelectedButton"),
|
|
107
|
+
promptListStatus: $("#promptListStatus"),
|
|
108
|
+
promptListCloseButton: $("#promptListCloseButton"),
|
|
109
|
+
promptListDialogLoadButton: $("#promptListDialogLoadButton"),
|
|
110
|
+
promptListSaveButton: $("#promptListSaveButton"),
|
|
111
|
+
promptListRunListButton: $("#promptListRunListButton"),
|
|
94
112
|
commandSearchInput: $("#commandSearchInput"),
|
|
95
113
|
commandsBox: $("#commandsBox"),
|
|
96
114
|
eventLog: $("#eventLog"),
|
|
@@ -103,6 +121,11 @@ const elements = {
|
|
|
103
121
|
pathPickerTitle: $("#pathPickerTitle"),
|
|
104
122
|
pathPickerCurrent: $("#pathPickerCurrent"),
|
|
105
123
|
pathPickerAddFastPickButton: $("#pathPickerAddFastPickButton"),
|
|
124
|
+
pathPickerCreateNameInput: $("#pathPickerCreateNameInput"),
|
|
125
|
+
pathPickerCreateButton: $("#pathPickerCreateButton"),
|
|
126
|
+
pathPickerSearchInput: $("#pathPickerSearchInput"),
|
|
127
|
+
pathPickerClearSearchButton: $("#pathPickerClearSearchButton"),
|
|
128
|
+
pathPickerSearchStatus: $("#pathPickerSearchStatus"),
|
|
106
129
|
pathPickerFastPicks: $("#pathPickerFastPicks"),
|
|
107
130
|
pathPickerRoots: $("#pathPickerRoots"),
|
|
108
131
|
pathPickerList: $("#pathPickerList"),
|
|
@@ -198,6 +221,7 @@ let blockedTabNotificationFallbackNoted = false;
|
|
|
198
221
|
let agentDoneNotificationsEnabled = false;
|
|
199
222
|
let thinkingOutputVisible = true;
|
|
200
223
|
let webuiSettings = {};
|
|
224
|
+
let busyPromptBehavior = "followUp";
|
|
201
225
|
let autocompleteMaxVisible = 12;
|
|
202
226
|
let doubleEscapeAction = "none";
|
|
203
227
|
let treeFilterMode = "default";
|
|
@@ -229,6 +253,8 @@ let abortRequestInFlight = false;
|
|
|
229
253
|
let userBashByTab = new Map();
|
|
230
254
|
let userBashQueuesByTab = new Map();
|
|
231
255
|
let latestQueuedMessagesByTab = new Map();
|
|
256
|
+
let loadedPromptList = null;
|
|
257
|
+
let promptListRunning = false;
|
|
232
258
|
let abortLongPressTimer = null;
|
|
233
259
|
let abortLongPressHandled = false;
|
|
234
260
|
const dialogQueue = [];
|
|
@@ -255,6 +281,7 @@ const GIT_FOOTER_WEBUI_PAYLOAD_VERSION = 1;
|
|
|
255
281
|
const GIT_FOOTER_WEBUI_PAYLOAD_CACHE_KEY = "pi-webui-git-footer-webui-payload-cache";
|
|
256
282
|
const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
|
|
257
283
|
const PROMPT_HISTORY_STORAGE_KEY = "pi-webui-prompt-history";
|
|
284
|
+
const PROMPT_LIST_STORAGE_KEY = "pi-webui-prompt-lists";
|
|
258
285
|
const PROMPT_HISTORY_LIMIT_PER_TAB = 50;
|
|
259
286
|
const ATTACHMENT_MAX_FILES = 12;
|
|
260
287
|
const ATTACHMENT_MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
@@ -287,7 +314,7 @@ const TODO_PROGRESS_PARTIAL_LINE_REGEX = /^\s*(?:(?:[-*]|\d+[.)])\s*)?\[(?: |x|X
|
|
|
287
314
|
const CHAT_SCROLL_KEYS = new Set(["ArrowDown", "ArrowUp", "End", "Home", "PageDown", "PageUp", " "]);
|
|
288
315
|
const TAB_ACTIVITY_IDLE_RECONCILE_GRACE_MS = 1200;
|
|
289
316
|
const FOREGROUND_RECONCILE_DELAY_MS = 120;
|
|
290
|
-
const TAB_GROUP_STATUS_PRIORITY = ["blocked", "done", "
|
|
317
|
+
const TAB_GROUP_STATUS_PRIORITY = ["blocked", "done", "working", "idle"];
|
|
291
318
|
const EXTENSION_UI_BLOCKING_METHODS = new Set(["select", "confirm", "input", "editor"]);
|
|
292
319
|
const BLOCKED_TAB_NOTIFICATION_TAG_PREFIX = "pi-webui-blocked-tab";
|
|
293
320
|
const AGENT_DONE_NOTIFICATION_TAG_PREFIX = "pi-webui-agent-done";
|
|
@@ -2560,7 +2587,7 @@ function restoreActiveDraft() {
|
|
|
2560
2587
|
|
|
2561
2588
|
function focusPromptInput({ defer = false } = {}) {
|
|
2562
2589
|
const focus = () => {
|
|
2563
|
-
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || elements.nativeCommandDialog.open || document.visibilityState === "hidden") return;
|
|
2590
|
+
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || elements.nativeCommandDialog.open || elements.promptListDialog?.open || document.visibilityState === "hidden") return;
|
|
2564
2591
|
try {
|
|
2565
2592
|
elements.promptInput.focus({ preventScroll: true });
|
|
2566
2593
|
} catch {
|
|
@@ -2631,10 +2658,7 @@ function resetActiveTabUi() {
|
|
|
2631
2658
|
elements.eventLog.replaceChildren();
|
|
2632
2659
|
const queuedSnapshot = activeTabId ? latestQueuedMessagesByTab.get(activeTabId) : null;
|
|
2633
2660
|
if (queuedSnapshot) renderQueue({ tabId: activeTabId, ...queuedSnapshot });
|
|
2634
|
-
else {
|
|
2635
|
-
elements.queueBox.textContent = "No queued messages.";
|
|
2636
|
-
elements.queueBox.classList.add("muted");
|
|
2637
|
-
}
|
|
2661
|
+
else renderQueue({ tabId: activeTabId, steering: [], followUp: [] });
|
|
2638
2662
|
elements.commandsBox.textContent = "Loading…";
|
|
2639
2663
|
elements.commandsBox.classList.add("muted");
|
|
2640
2664
|
renderWidgets();
|
|
@@ -2773,8 +2797,9 @@ function renderTerminalTabGroup(group, groupCount = 1) {
|
|
|
2773
2797
|
const isActive = groupTabs.some((tab) => tab.id === activeTabId);
|
|
2774
2798
|
const isStopped = groupTabs.every((tab) => !tab.running);
|
|
2775
2799
|
const indicator = tabGroupIndicator(groupTabs);
|
|
2776
|
-
const
|
|
2777
|
-
const
|
|
2800
|
+
const cwdTitle = tabGroupTitle(group.cwd, activeGroupTab?.title || "cwd");
|
|
2801
|
+
const activeTitle = activeGroupTab?.title || cwdTitle;
|
|
2802
|
+
const displayCwd = normalizeDisplayPath(group.cwd || cwdTitle);
|
|
2778
2803
|
const wrapper = make("div", `terminal-tab terminal-tab-group activity-${indicator.state}${isActive ? " active" : ""}${isStopped ? " stopped" : ""}`);
|
|
2779
2804
|
wrapper.dataset.groupKey = group.key;
|
|
2780
2805
|
wrapper.addEventListener("pointerenter", () => setOpenTerminalTabGroup(group.key));
|
|
@@ -2791,9 +2816,9 @@ function renderTerminalTabGroup(group, groupCount = 1) {
|
|
|
2791
2816
|
button.setAttribute("aria-selected", isActive ? "true" : "false");
|
|
2792
2817
|
button.setAttribute("aria-haspopup", "true");
|
|
2793
2818
|
button.setAttribute("aria-expanded", group.key === openTerminalTabGroupKey ? "true" : "false");
|
|
2794
|
-
button.setAttribute("aria-label", `${
|
|
2795
|
-
button.title = `${displayCwd} · ${groupTabs.length} tabs · ${indicator.label}`;
|
|
2796
|
-
appendTerminalTabContent(button, { title, indicator, meta: `${
|
|
2819
|
+
button.setAttribute("aria-label", `${cwdTitle} group: ${groupTabs.length} tabs, ${indicator.label}. Active ${activeTitle}`);
|
|
2820
|
+
button.title = `${activeTitle} · ${displayCwd} · ${groupTabs.length} tabs · ${indicator.label}`;
|
|
2821
|
+
appendTerminalTabContent(button, { title: activeTitle, indicator, meta: `${cwdTitle} · ${indicator.meta}`, count: groupTabs.length });
|
|
2797
2822
|
button.addEventListener("click", () => switchTab(activeGroupTab.id));
|
|
2798
2823
|
wrapper.append(button);
|
|
2799
2824
|
|
|
@@ -2854,6 +2879,8 @@ function renderTabs() {
|
|
|
2854
2879
|
elements.terminalTabsToggleButton.textContent = active ? `${activeIndicator.glyph} ${active.title}${tabs.length > 1 ? ` · ${tabs.length}` : ""}` : "Tabs";
|
|
2855
2880
|
elements.terminalTabsToggleButton.title = active ? `Show terminal tabs · active: ${active.title} · ${activeIndicator.label}` : "Show terminal tabs";
|
|
2856
2881
|
elements.tabBar.replaceChildren();
|
|
2882
|
+
elements.tabBar.dataset.tabCount = String(tabs.length);
|
|
2883
|
+
elements.tabBar.classList.toggle("terminal-tabs-dense", tabs.length >= 10);
|
|
2857
2884
|
const groups = tabCwdGroups();
|
|
2858
2885
|
const renderedGroupKeys = new Set(groups.filter((group) => shouldRenderTerminalTabGroup(group, groups.length)).map((group) => group.key));
|
|
2859
2886
|
if (openTerminalTabGroupKey && !renderedGroupKeys.has(openTerminalTabGroupKey)) openTerminalTabGroupKey = null;
|
|
@@ -3518,14 +3545,20 @@ function footerStatsCostDisplay(stats = latestStats) {
|
|
|
3518
3545
|
return `$${Number(stats.cost || 0).toFixed(3)} (${footerCostAuthLabel()})`;
|
|
3519
3546
|
}
|
|
3520
3547
|
|
|
3548
|
+
function footerContextDisplayWithAuto(value, state = currentState) {
|
|
3549
|
+
const text = cleanStatusText(value);
|
|
3550
|
+
if (!text) return "";
|
|
3551
|
+
const withoutAuto = text.replace(/\s*\(auto\)\s*$/i, "");
|
|
3552
|
+
return state?.autoCompactionEnabled !== false ? `${withoutAuto} (auto)` : withoutAuto;
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3521
3555
|
function footerStatsContextDisplay(stats = latestStats) {
|
|
3522
3556
|
const usage = stats?.contextUsage || currentState?.contextUsage;
|
|
3523
3557
|
const contextWindow = usage?.contextWindow ?? currentState?.model?.contextWindow ?? 0;
|
|
3524
3558
|
if (!contextWindow) return "";
|
|
3525
3559
|
const rawPercent = Number(usage?.percent);
|
|
3526
3560
|
const percent = Number.isFinite(rawPercent) ? `${rawPercent.toFixed(1)}%` : "?";
|
|
3527
|
-
|
|
3528
|
-
return `${percent}/${formatFooterTokenCount(contextWindow)}${auto}`;
|
|
3561
|
+
return footerContextDisplayWithAuto(`${percent}/${formatFooterTokenCount(contextWindow)}`);
|
|
3529
3562
|
}
|
|
3530
3563
|
|
|
3531
3564
|
function fallbackFooterStats() {
|
|
@@ -3560,11 +3593,89 @@ function textFromContent(content) {
|
|
|
3560
3593
|
.join("\n");
|
|
3561
3594
|
}
|
|
3562
3595
|
|
|
3596
|
+
function cleanTooltipText(value) {
|
|
3597
|
+
return stripAnsi(value).replace(/\r\n?/g, "\n").replace(/[^\S\n]+/g, " ").replace(/\n{3,}/g, "\n\n").trim();
|
|
3598
|
+
}
|
|
3599
|
+
|
|
3600
|
+
let footerTooltipNode = null;
|
|
3601
|
+
let footerTooltipTarget = null;
|
|
3602
|
+
let footerTooltipEventsBound = false;
|
|
3603
|
+
|
|
3604
|
+
function ensureFooterTooltipNode() {
|
|
3605
|
+
if (!footerTooltipNode) {
|
|
3606
|
+
footerTooltipNode = make("div", "footer-floating-tooltip");
|
|
3607
|
+
footerTooltipNode.hidden = true;
|
|
3608
|
+
document.body.append(footerTooltipNode);
|
|
3609
|
+
}
|
|
3610
|
+
if (!footerTooltipEventsBound) {
|
|
3611
|
+
footerTooltipEventsBound = true;
|
|
3612
|
+
const update = () => positionFooterTooltip(footerTooltipTarget);
|
|
3613
|
+
window.addEventListener("resize", update, { passive: true });
|
|
3614
|
+
window.addEventListener("scroll", update, { passive: true, capture: true });
|
|
3615
|
+
}
|
|
3616
|
+
return footerTooltipNode;
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
function positionFooterTooltip(target) {
|
|
3620
|
+
if (!target || !footerTooltipNode || footerTooltipNode.hidden) return;
|
|
3621
|
+
const gap = 8;
|
|
3622
|
+
const margin = 8;
|
|
3623
|
+
const rect = target.getBoundingClientRect();
|
|
3624
|
+
const maxWidth = Math.max(96, Math.min(384, window.innerWidth - margin * 2));
|
|
3625
|
+
footerTooltipNode.style.maxWidth = `${maxWidth}px`;
|
|
3626
|
+
|
|
3627
|
+
const width = footerTooltipNode.offsetWidth;
|
|
3628
|
+
const height = footerTooltipNode.offsetHeight;
|
|
3629
|
+
const align = target.getAttribute("data-tooltip-align") || "center";
|
|
3630
|
+
let left = rect.left + rect.width / 2 - width / 2;
|
|
3631
|
+
if (align === "start") left = rect.left;
|
|
3632
|
+
if (align === "end") left = rect.right - width;
|
|
3633
|
+
left = Math.min(Math.max(margin, left), Math.max(margin, window.innerWidth - margin - width));
|
|
3634
|
+
|
|
3635
|
+
let top = rect.top - gap - height;
|
|
3636
|
+
if (top < margin) top = rect.bottom + gap;
|
|
3637
|
+
top = Math.min(Math.max(margin, top), window.innerHeight - margin - height);
|
|
3638
|
+
|
|
3639
|
+
footerTooltipNode.style.left = `${Math.round(left)}px`;
|
|
3640
|
+
footerTooltipNode.style.top = `${Math.round(top)}px`;
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
function showFooterTooltip(target) {
|
|
3644
|
+
const text = target?.getAttribute("data-tooltip");
|
|
3645
|
+
if (!text) return;
|
|
3646
|
+
footerTooltipTarget = target;
|
|
3647
|
+
const tooltip = ensureFooterTooltipNode();
|
|
3648
|
+
tooltip.textContent = text;
|
|
3649
|
+
tooltip.hidden = false;
|
|
3650
|
+
tooltip.classList.add("visible");
|
|
3651
|
+
positionFooterTooltip(target);
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
function hideFooterTooltip(target) {
|
|
3655
|
+
if (target && target !== footerTooltipTarget) return;
|
|
3656
|
+
footerTooltipTarget = null;
|
|
3657
|
+
if (!footerTooltipNode) return;
|
|
3658
|
+
footerTooltipNode.hidden = true;
|
|
3659
|
+
footerTooltipNode.classList.remove("visible");
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3662
|
+
function applyFooterTooltip(node, tooltip, options = {}) {
|
|
3663
|
+
const text = cleanTooltipText(tooltip);
|
|
3664
|
+
if (!text) return node;
|
|
3665
|
+
node.setAttribute("data-tooltip", text);
|
|
3666
|
+
node.setAttribute("aria-label", text.replace(/\s+/g, " "));
|
|
3667
|
+
if (options.align) node.setAttribute("data-tooltip-align", options.align);
|
|
3668
|
+
node.addEventListener("mouseenter", () => showFooterTooltip(node));
|
|
3669
|
+
node.addEventListener("mouseleave", () => hideFooterTooltip(node));
|
|
3670
|
+
node.addEventListener("focus", () => showFooterTooltip(node));
|
|
3671
|
+
node.addEventListener("blur", () => hideFooterTooltip(node));
|
|
3672
|
+
return node;
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3563
3675
|
function footerMetric(icon, label, value, tone = "", options = {}) {
|
|
3564
3676
|
const node = make("span", `footer-metric ${tone}`.trim());
|
|
3565
3677
|
node.append(make("span", "footer-metric-icon", icon), make("span", "footer-metric-label", label), make("span", "footer-metric-value", value));
|
|
3566
|
-
node
|
|
3567
|
-
return node;
|
|
3678
|
+
return applyFooterTooltip(node, options.title || `${label}: ${value}`, { align: options.tooltipAlign });
|
|
3568
3679
|
}
|
|
3569
3680
|
|
|
3570
3681
|
function contextUsageActiveColor(percent) {
|
|
@@ -3610,8 +3721,7 @@ function footerMeta(label, value, className = "", options = {}) {
|
|
|
3610
3721
|
node.addEventListener("click", options.onClick);
|
|
3611
3722
|
}
|
|
3612
3723
|
node.append(make("span", "footer-meta-label", label), make("span", "footer-meta-value", value));
|
|
3613
|
-
node
|
|
3614
|
-
return node;
|
|
3724
|
+
return applyFooterTooltip(node, options.title || `${label}: ${value}`, { align: options.tooltipAlign });
|
|
3615
3725
|
}
|
|
3616
3726
|
|
|
3617
3727
|
const FOOTER_PAYLOAD_TONES = new Set(["pink", "blue", "mauve", "yellow", "green", "teal"]);
|
|
@@ -3627,8 +3737,25 @@ const FOOTER_META_CLASS_BY_KEY = new Map([
|
|
|
3627
3737
|
["thinking", "footer-thinking"],
|
|
3628
3738
|
]);
|
|
3629
3739
|
|
|
3630
|
-
|
|
3631
|
-
|
|
3740
|
+
const GIT_FOOTER_TOOLTIP_COPY = {
|
|
3741
|
+
tokens: "Session token totals. ↑ is input/prompt tokens; ↓ is assistant output tokens.",
|
|
3742
|
+
cache: "Provider prompt-cache usage. R is cache-read tokens; W is cache-write tokens.",
|
|
3743
|
+
pi: "Estimated initial Pi prompt size before your message. … means the estimate is still pending.",
|
|
3744
|
+
speed: "Assistant streaming speed. Shows live output tokens for the current reply and current or last tokens per second.",
|
|
3745
|
+
cost: "Estimated session cost. sub means subscription-backed provider; api means metered API usage.",
|
|
3746
|
+
context: "Context window pressure. Shows percent used over the model limit; auto means auto-compaction is enabled.",
|
|
3747
|
+
cwd: "Active working directory for this Web UI tab.",
|
|
3748
|
+
git: "Current Git branch. detached means HEAD is not on a branch; no repo means the cwd is outside a Git work tree.",
|
|
3749
|
+
"git-state": "Active Git operation or detached state. Finish or abort rebase/merge/cherry-pick/revert/bisect before normal commits.",
|
|
3750
|
+
sync: "Remote tracking divergence. ↑ means local commits ahead; ↓ means remote commits to pull.",
|
|
3751
|
+
changes: "Working tree summary. ✅ staged, ✏️ modified unstaged, ➕ untracked, ⚠️ conflicted; clean means no changes.",
|
|
3752
|
+
"git-extra": "Extra Git signals. 📦 stash, 🧩 dirty submodules, 🌳 worktrees, 🏷️ tag at HEAD, 🕒 last commit age, 🔓 signing mismatch.",
|
|
3753
|
+
model: "Scoped model for this tab.",
|
|
3754
|
+
thinking: "Reasoning/thinking effort for this tab.",
|
|
3755
|
+
};
|
|
3756
|
+
|
|
3757
|
+
function cleanFooterPayloadText(value, fallback = "", maxLength = 240) {
|
|
3758
|
+
const text = cleanStatusText(value).slice(0, maxLength);
|
|
3632
3759
|
return text || fallback;
|
|
3633
3760
|
}
|
|
3634
3761
|
|
|
@@ -3642,7 +3769,7 @@ function normalizeFooterPayloadChip(value, index) {
|
|
|
3642
3769
|
value: cleanFooterPayloadText(value.value, "—"),
|
|
3643
3770
|
icon: cleanFooterPayloadText(value.icon, "•").slice(0, 8),
|
|
3644
3771
|
tone: FOOTER_PAYLOAD_TONES.has(value.tone) ? value.tone : "",
|
|
3645
|
-
title: cleanFooterPayloadText(value.title, ""),
|
|
3772
|
+
title: cleanFooterPayloadText(value.title, "", 4000),
|
|
3646
3773
|
};
|
|
3647
3774
|
if (value.contextUsage && typeof value.contextUsage === "object") {
|
|
3648
3775
|
const percent = typeof value.contextUsage.percent === "number" ? value.contextUsage.percent : Number.NaN;
|
|
@@ -3721,14 +3848,19 @@ function parseGitFooterWebuiPayload() {
|
|
|
3721
3848
|
}
|
|
3722
3849
|
|
|
3723
3850
|
function footerPayloadWithLiveModel(payload) {
|
|
3724
|
-
if (!payload
|
|
3725
|
-
const model = shortModelLabel(currentState.model);
|
|
3851
|
+
if (!payload) return payload;
|
|
3852
|
+
const model = currentState?.model ? shortModelLabel(currentState.model) : "";
|
|
3726
3853
|
const effort = footerThinkingDisplay();
|
|
3727
3854
|
const hasThinkingChip = [...payload.main, ...payload.meta].some((chip) => chip?.key === "thinking");
|
|
3855
|
+
const contextChip = (chip) => {
|
|
3856
|
+
const value = footerContextDisplayWithAuto(chip?.value);
|
|
3857
|
+
return { ...chip, value, title: `context: ${value}` };
|
|
3858
|
+
};
|
|
3728
3859
|
const effortChip = (chip) => ({ ...chip, key: "thinking", label: "effort", value: effort, title: `effort: ${effort}`, tone: "mauve" });
|
|
3729
3860
|
const splitChip = (chip) => {
|
|
3861
|
+
if (chip?.key === "context") return [contextChip(chip)];
|
|
3730
3862
|
if (chip?.key === "thinking") return [effortChip(chip)];
|
|
3731
|
-
if (chip?.key !== "model") return [chip];
|
|
3863
|
+
if (chip?.key !== "model" || !model) return [chip];
|
|
3732
3864
|
const modelChip = { ...chip, value: model, title: `model: ${model}` };
|
|
3733
3865
|
return hasThinkingChip ? [modelChip] : [modelChip, effortChip(chip)];
|
|
3734
3866
|
};
|
|
@@ -3744,6 +3876,30 @@ function footerMetaClassForPayload(chip) {
|
|
|
3744
3876
|
return `${base}${toneClass}`.trim();
|
|
3745
3877
|
}
|
|
3746
3878
|
|
|
3879
|
+
function isRedundantFooterTooltipTitle(sourceTitle, chip, value) {
|
|
3880
|
+
const normalized = cleanStatusText(sourceTitle).toLowerCase();
|
|
3881
|
+
if (!normalized) return true;
|
|
3882
|
+
const labels = [chip?.label, chip?.key].map((item) => cleanFooterPayloadText(item, "").toLowerCase()).filter(Boolean);
|
|
3883
|
+
return [`current: ${value}`, ...labels.map((label) => `${label}: ${value}`)].some((item) => normalized === cleanStatusText(item).toLowerCase());
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
function gitFooterPayloadTooltip(chip, options = {}) {
|
|
3887
|
+
const key = cleanFooterPayloadText(chip?.key, "");
|
|
3888
|
+
const value = cleanFooterPayloadText(chip?.value, "—");
|
|
3889
|
+
const sourceTitle = cleanFooterPayloadText(chip?.title, "", 4000);
|
|
3890
|
+
const action = cleanFooterPayloadText(options.action, "", 1000);
|
|
3891
|
+
const parts = [GIT_FOOTER_TOOLTIP_COPY[key] || "Extension-provided git-footer-status item.", `Current: ${value}`];
|
|
3892
|
+
if (sourceTitle && !isRedundantFooterTooltipTitle(sourceTitle, chip, value)) parts.push(sourceTitle);
|
|
3893
|
+
if (action) parts.push(action);
|
|
3894
|
+
return parts.join("\n");
|
|
3895
|
+
}
|
|
3896
|
+
|
|
3897
|
+
function gitFooterTooltipAlign(chip) {
|
|
3898
|
+
if (["tokens", "cwd"].includes(chip?.key)) return "start";
|
|
3899
|
+
if (["model", "thinking"].includes(chip?.key)) return "end";
|
|
3900
|
+
return "center";
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3747
3903
|
function footerTuiItem(value, className = "", options = {}) {
|
|
3748
3904
|
const text = cleanStatusText(value);
|
|
3749
3905
|
const isAction = typeof options.onClick === "function";
|
|
@@ -3776,28 +3932,35 @@ function renderTuiFooterLine({ cwd, cwdTitle, message = "", stats = [], model =
|
|
|
3776
3932
|
}
|
|
3777
3933
|
|
|
3778
3934
|
function renderGitFooterPayloadMetric(chip) {
|
|
3779
|
-
const node = footerMetric(chip.icon || "•", chip.label, chip.value, chip.tone ? `tone-${chip.tone}` : "", {
|
|
3935
|
+
const node = footerMetric(chip.icon || "•", chip.label, chip.value, chip.tone ? `tone-${chip.tone}` : "", {
|
|
3936
|
+
title: gitFooterPayloadTooltip(chip),
|
|
3937
|
+
tooltipAlign: gitFooterTooltipAlign(chip),
|
|
3938
|
+
});
|
|
3780
3939
|
return chip.contextUsage ? applyFooterContextUsage(node, chip.contextUsage) : node;
|
|
3781
3940
|
}
|
|
3782
3941
|
|
|
3783
3942
|
function renderGitFooterPayloadMeta(chip, tab) {
|
|
3784
|
-
const options = {
|
|
3943
|
+
const options = {};
|
|
3944
|
+
let action = "";
|
|
3785
3945
|
if (chip.key === "cwd" && tab) {
|
|
3786
3946
|
options.onClick = changeActiveTabCwd;
|
|
3787
|
-
|
|
3947
|
+
action = `Click to change the working directory for ${tab.title}.`;
|
|
3788
3948
|
} else if (chip.key === "model") {
|
|
3789
3949
|
options.onClick = () => setFooterModelPickerOpen(!footerModelPickerOpen);
|
|
3790
|
-
|
|
3950
|
+
action = "Click to choose another model.";
|
|
3791
3951
|
} else if (chip.key === "thinking") {
|
|
3792
3952
|
options.onClick = () => setFooterThinkingPickerOpen(!footerThinkingPickerOpen);
|
|
3793
|
-
|
|
3953
|
+
action = "Click to change thinking effort.";
|
|
3794
3954
|
}
|
|
3955
|
+
options.title = gitFooterPayloadTooltip(chip, { action });
|
|
3956
|
+
options.tooltipAlign = gitFooterTooltipAlign(chip);
|
|
3795
3957
|
const node = footerMeta(chip.label, chip.value, footerMetaClassForPayload(chip), options);
|
|
3796
3958
|
return chip.contextUsage ? applyFooterContextUsage(node, chip.contextUsage) : node;
|
|
3797
3959
|
}
|
|
3798
3960
|
|
|
3799
3961
|
function renderGitFooterPayload(payload) {
|
|
3800
3962
|
const tab = activeTab();
|
|
3963
|
+
hideFooterTooltip();
|
|
3801
3964
|
elements.statusBar.replaceChildren();
|
|
3802
3965
|
elements.statusBar.classList.remove("statusbar-tui-footer");
|
|
3803
3966
|
elements.statusBar.classList.add("statusbar-git-footer");
|
|
@@ -3833,6 +3996,7 @@ function gitFooterFallbackMessage() {
|
|
|
3833
3996
|
}
|
|
3834
3997
|
|
|
3835
3998
|
function renderMinimalFooter() {
|
|
3999
|
+
hideFooterTooltip();
|
|
3836
4000
|
const tab = activeTab();
|
|
3837
4001
|
const workspaceLabel = latestWorkspace?.displayCwd || (tab?.cwd ? normalizeDisplayPath(tab.cwd) : "loading…");
|
|
3838
4002
|
const modelLine = footerModelLine();
|
|
@@ -3997,6 +4161,81 @@ function setPathPickerError(message) {
|
|
|
3997
4161
|
elements.pathPickerError.hidden = !message;
|
|
3998
4162
|
}
|
|
3999
4163
|
|
|
4164
|
+
function pathPickerCreateName() {
|
|
4165
|
+
return elements.pathPickerCreateNameInput.value.trim();
|
|
4166
|
+
}
|
|
4167
|
+
|
|
4168
|
+
function pathPickerCreateValidationError(name = pathPickerCreateName()) {
|
|
4169
|
+
if (!name) return "Enter a directory name.";
|
|
4170
|
+
if (name === "." || name === "..") return "Use a real directory name, not . or ..";
|
|
4171
|
+
if (name.includes("\u0000")) return "Directory names cannot contain null bytes.";
|
|
4172
|
+
if (/[\\/]/.test(name)) return "Create one directory at a time; do not include path separators.";
|
|
4173
|
+
return "";
|
|
4174
|
+
}
|
|
4175
|
+
|
|
4176
|
+
function updateCreateDirectoryControls() {
|
|
4177
|
+
const busy = !!pathPickerState?.creatingDirectory;
|
|
4178
|
+
const loading = !!pathPickerState?.loading;
|
|
4179
|
+
const canCreate = !!pathPickerState?.cwd && !loading && !busy && !!pathPickerCreateName();
|
|
4180
|
+
elements.pathPickerCreateNameInput.disabled = !pathPickerState || loading || busy;
|
|
4181
|
+
elements.pathPickerCreateButton.disabled = !canCreate;
|
|
4182
|
+
elements.pathPickerCreateButton.textContent = busy ? "Creating…" : "Create directory";
|
|
4183
|
+
}
|
|
4184
|
+
|
|
4185
|
+
function pathPickerSearchQuery() {
|
|
4186
|
+
return elements.pathPickerSearchInput.value.trim().toLowerCase();
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
function updatePathPickerSearchControls() {
|
|
4190
|
+
const loading = !!pathPickerState?.loading;
|
|
4191
|
+
const hasDirectory = !!pathPickerState?.cwd;
|
|
4192
|
+
const hasQuery = !!pathPickerSearchQuery();
|
|
4193
|
+
elements.pathPickerSearchInput.disabled = !pathPickerState || loading || !hasDirectory;
|
|
4194
|
+
elements.pathPickerClearSearchButton.hidden = !hasQuery;
|
|
4195
|
+
elements.pathPickerClearSearchButton.disabled = loading || !hasQuery;
|
|
4196
|
+
}
|
|
4197
|
+
|
|
4198
|
+
function pathPickerDirectoryMatchesSearch(directory, query) {
|
|
4199
|
+
if (!query) return true;
|
|
4200
|
+
const haystack = String(directory?.name || "").toLowerCase();
|
|
4201
|
+
return query.split(/\s+/).every((term) => haystack.includes(term));
|
|
4202
|
+
}
|
|
4203
|
+
|
|
4204
|
+
function renderPathPickerDirectoryList() {
|
|
4205
|
+
if (!pathPickerState) return;
|
|
4206
|
+
const directories = Array.isArray(pathPickerState.directories) ? pathPickerState.directories : [];
|
|
4207
|
+
const query = pathPickerSearchQuery();
|
|
4208
|
+
const matches = directories.filter((directory) => pathPickerDirectoryMatchesSearch(directory, query));
|
|
4209
|
+
pathPickerState.filteredDirectories = matches;
|
|
4210
|
+
updatePathPickerSearchControls();
|
|
4211
|
+
elements.pathPickerList.replaceChildren();
|
|
4212
|
+
|
|
4213
|
+
if (query) {
|
|
4214
|
+
elements.pathPickerSearchStatus.textContent = matches.length === directories.length
|
|
4215
|
+
? `Showing all ${matches.length} director${matches.length === 1 ? "y" : "ies"}.`
|
|
4216
|
+
: `Showing ${matches.length} of ${directories.length} directories.`;
|
|
4217
|
+
} else {
|
|
4218
|
+
elements.pathPickerSearchStatus.textContent = "";
|
|
4219
|
+
}
|
|
4220
|
+
|
|
4221
|
+
if (!matches.length) {
|
|
4222
|
+
elements.pathPickerList.append(make("div", "path-picker-empty muted", query ? `No directories match “${elements.pathPickerSearchInput.value.trim()}”.` : "No subdirectories."));
|
|
4223
|
+
return;
|
|
4224
|
+
}
|
|
4225
|
+
|
|
4226
|
+
for (const directory of matches) {
|
|
4227
|
+
const button = pathPickerButton(`${directory.name}/`, directory.cwd, () => loadPathPickerDirectory(directory.cwd), `path-picker-directory${directory.hidden ? " hidden-directory" : ""}`);
|
|
4228
|
+
button.setAttribute("role", "option");
|
|
4229
|
+
elements.pathPickerList.append(button);
|
|
4230
|
+
}
|
|
4231
|
+
}
|
|
4232
|
+
|
|
4233
|
+
function clearPathPickerSearch({ focus = false } = {}) {
|
|
4234
|
+
elements.pathPickerSearchInput.value = "";
|
|
4235
|
+
renderPathPickerDirectoryList();
|
|
4236
|
+
if (focus) elements.pathPickerSearchInput.focus();
|
|
4237
|
+
}
|
|
4238
|
+
|
|
4000
4239
|
function normalizeFastPicks(value) {
|
|
4001
4240
|
const items = Array.isArray(value) ? value : [];
|
|
4002
4241
|
const seen = new Set();
|
|
@@ -4137,11 +4376,17 @@ async function addCurrentFastPick() {
|
|
|
4137
4376
|
function renderPathPicker(data) {
|
|
4138
4377
|
if (!pathPickerState) return;
|
|
4139
4378
|
pathPickerState.cwd = data.cwd;
|
|
4379
|
+
pathPickerState.loading = false;
|
|
4380
|
+
pathPickerState.directories = Array.isArray(data.directories) ? data.directories : [];
|
|
4381
|
+
pathPickerState.filteredDirectories = pathPickerState.directories;
|
|
4140
4382
|
elements.pathPickerCurrent.textContent = data.displayCwd || data.cwd;
|
|
4141
4383
|
elements.pathPickerCurrent.title = data.cwd;
|
|
4142
4384
|
elements.pathPickerChooseButton.disabled = false;
|
|
4143
4385
|
elements.pathPickerChooseButton.textContent = "Use this directory";
|
|
4386
|
+
elements.pathPickerCreateNameInput.value = "";
|
|
4387
|
+
elements.pathPickerSearchInput.value = "";
|
|
4144
4388
|
setPathPickerError(data.truncated ? "Showing the first 500 directories." : "");
|
|
4389
|
+
updateCreateDirectoryControls();
|
|
4145
4390
|
renderFastPicks();
|
|
4146
4391
|
|
|
4147
4392
|
elements.pathPickerRoots.replaceChildren();
|
|
@@ -4152,25 +4397,19 @@ function renderPathPicker(data) {
|
|
|
4152
4397
|
elements.pathPickerRoots.append(pathPickerButton(root.label, root.cwd, () => loadPathPickerDirectory(root.cwd), "path-picker-root-button"));
|
|
4153
4398
|
}
|
|
4154
4399
|
|
|
4155
|
-
|
|
4156
|
-
if (!data.directories?.length) {
|
|
4157
|
-
elements.pathPickerList.append(make("div", "path-picker-empty muted", "No subdirectories."));
|
|
4158
|
-
return;
|
|
4159
|
-
}
|
|
4160
|
-
|
|
4161
|
-
for (const directory of data.directories) {
|
|
4162
|
-
const button = pathPickerButton(`${directory.name}/`, directory.cwd, () => loadPathPickerDirectory(directory.cwd), `path-picker-directory${directory.hidden ? " hidden-directory" : ""}`);
|
|
4163
|
-
button.setAttribute("role", "option");
|
|
4164
|
-
elements.pathPickerList.append(button);
|
|
4165
|
-
}
|
|
4400
|
+
renderPathPickerDirectoryList();
|
|
4166
4401
|
}
|
|
4167
4402
|
|
|
4168
4403
|
async function loadPathPickerDirectory(cwd) {
|
|
4169
4404
|
if (!pathPickerState) return;
|
|
4170
4405
|
const requestId = ++pathPickerState.requestId;
|
|
4406
|
+
pathPickerState.loading = true;
|
|
4171
4407
|
elements.pathPickerAddFastPickButton.disabled = true;
|
|
4172
4408
|
elements.pathPickerChooseButton.disabled = true;
|
|
4173
4409
|
elements.pathPickerCurrent.textContent = "Loading…";
|
|
4410
|
+
elements.pathPickerSearchStatus.textContent = "";
|
|
4411
|
+
updateCreateDirectoryControls();
|
|
4412
|
+
updatePathPickerSearchControls();
|
|
4174
4413
|
setPathPickerError("");
|
|
4175
4414
|
|
|
4176
4415
|
try {
|
|
@@ -4180,13 +4419,48 @@ async function loadPathPickerDirectory(cwd) {
|
|
|
4180
4419
|
renderPathPicker(response.data || {});
|
|
4181
4420
|
} catch (error) {
|
|
4182
4421
|
if (!pathPickerState || pathPickerState.requestId !== requestId) return;
|
|
4422
|
+
pathPickerState.loading = false;
|
|
4183
4423
|
elements.pathPickerChooseButton.disabled = false;
|
|
4184
4424
|
elements.pathPickerCurrent.textContent = pathPickerState.cwd || "Unable to load directory";
|
|
4185
4425
|
setPathPickerError(error.message);
|
|
4426
|
+
updateCreateDirectoryControls();
|
|
4427
|
+
updatePathPickerSearchControls();
|
|
4186
4428
|
updateAddFastPickButton();
|
|
4187
4429
|
}
|
|
4188
4430
|
}
|
|
4189
4431
|
|
|
4432
|
+
async function createPathPickerDirectory() {
|
|
4433
|
+
const state = pathPickerState;
|
|
4434
|
+
if (!state?.cwd || state.loading || state.creatingDirectory) return;
|
|
4435
|
+
const name = pathPickerCreateName();
|
|
4436
|
+
const validationError = pathPickerCreateValidationError(name);
|
|
4437
|
+
if (validationError) {
|
|
4438
|
+
setPathPickerError(validationError);
|
|
4439
|
+
elements.pathPickerCreateNameInput.focus();
|
|
4440
|
+
return;
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
const requestId = ++state.requestId;
|
|
4444
|
+
state.creatingDirectory = true;
|
|
4445
|
+
updateCreateDirectoryControls();
|
|
4446
|
+
setPathPickerError("");
|
|
4447
|
+
try {
|
|
4448
|
+
const response = await api("/api/directories", { method: "POST", body: { parent: state.cwd, name } });
|
|
4449
|
+
if (!pathPickerState || pathPickerState !== state || state.requestId !== requestId) return;
|
|
4450
|
+
renderPathPicker(response.data || {});
|
|
4451
|
+
elements.pathPickerChooseButton.focus({ preventScroll: true });
|
|
4452
|
+
} catch (error) {
|
|
4453
|
+
if (!pathPickerState || pathPickerState !== state || state.requestId !== requestId) return;
|
|
4454
|
+
setPathPickerError(error.message);
|
|
4455
|
+
elements.pathPickerCreateNameInput.focus();
|
|
4456
|
+
} finally {
|
|
4457
|
+
if (pathPickerState === state) {
|
|
4458
|
+
state.creatingDirectory = false;
|
|
4459
|
+
updateCreateDirectoryControls();
|
|
4460
|
+
}
|
|
4461
|
+
}
|
|
4462
|
+
}
|
|
4463
|
+
|
|
4190
4464
|
function closePathPicker(cwd) {
|
|
4191
4465
|
const state = pathPickerState;
|
|
4192
4466
|
if (!state) return;
|
|
@@ -4199,15 +4473,20 @@ function pickCwd(tab, initialCwd) {
|
|
|
4199
4473
|
if (pathPickerState) return Promise.resolve(null);
|
|
4200
4474
|
|
|
4201
4475
|
return new Promise((resolve) => {
|
|
4202
|
-
pathPickerState = { tabId: tab.id, cwd: initialCwd, requestId: 0, resolve };
|
|
4476
|
+
pathPickerState = { tabId: tab.id, cwd: initialCwd, requestId: 0, loading: false, creatingDirectory: false, directories: [], filteredDirectories: [], resolve };
|
|
4203
4477
|
elements.pathPickerTitle.textContent = `Choose CWD for ${tab.title}`;
|
|
4204
4478
|
elements.pathPickerCurrent.textContent = "Loading…";
|
|
4479
|
+
elements.pathPickerCreateNameInput.value = "";
|
|
4480
|
+
elements.pathPickerSearchInput.value = "";
|
|
4481
|
+
elements.pathPickerSearchStatus.textContent = "";
|
|
4205
4482
|
elements.pathPickerFastPicks.replaceChildren();
|
|
4206
4483
|
elements.pathPickerRoots.replaceChildren();
|
|
4207
4484
|
elements.pathPickerList.replaceChildren();
|
|
4208
4485
|
setPathPickerError("");
|
|
4209
4486
|
elements.pathPickerAddFastPickButton.disabled = true;
|
|
4210
4487
|
elements.pathPickerChooseButton.disabled = true;
|
|
4488
|
+
updateCreateDirectoryControls();
|
|
4489
|
+
updatePathPickerSearchControls();
|
|
4211
4490
|
initializeFastPicks().catch((error) => addEvent(`failed to initialize path fast picks: ${error.message}`, "error"));
|
|
4212
4491
|
elements.pathPickerDialog.showModal();
|
|
4213
4492
|
loadPathPickerDirectory(initialCwd);
|
|
@@ -5283,23 +5562,72 @@ function normalizeQueuedMessages(event) {
|
|
|
5283
5562
|
};
|
|
5284
5563
|
}
|
|
5285
5564
|
|
|
5565
|
+
function queueMessageCount(snapshot) {
|
|
5566
|
+
return (snapshot?.steering?.length || 0) + (snapshot?.followUp?.length || 0);
|
|
5567
|
+
}
|
|
5568
|
+
|
|
5569
|
+
function updateQueueHeaderBadge(total) {
|
|
5570
|
+
if (!elements.queueCountBadge) return;
|
|
5571
|
+
elements.queueCountBadge.hidden = total === 0;
|
|
5572
|
+
elements.queueCountBadge.textContent = String(total);
|
|
5573
|
+
elements.queueCountBadge.setAttribute("aria-label", `${total} queued message${total === 1 ? "" : "s"}`);
|
|
5574
|
+
}
|
|
5575
|
+
|
|
5576
|
+
function queueSummaryPill(label, count, tone) {
|
|
5577
|
+
const pill = make("span", `queue-summary-pill ${tone}`.trim());
|
|
5578
|
+
pill.append(make("strong", undefined, String(count)), make("span", undefined, label));
|
|
5579
|
+
return pill;
|
|
5580
|
+
}
|
|
5581
|
+
|
|
5582
|
+
function renderQueueGroup(label, items, tone) {
|
|
5583
|
+
const group = make("section", `queue-group ${tone}`.trim());
|
|
5584
|
+
const heading = make("h3", "queue-group-title");
|
|
5585
|
+
heading.append(make("span", undefined, label), make("span", "queue-group-count", String(items.length)));
|
|
5586
|
+
const list = make("ol", "queue-list");
|
|
5587
|
+
items.forEach((item, index) => {
|
|
5588
|
+
const row = make("li", "queue-item");
|
|
5589
|
+
row.append(make("span", "queue-item-number", `#${index + 1}`), make("div", "queue-item-text", item));
|
|
5590
|
+
list.append(row);
|
|
5591
|
+
});
|
|
5592
|
+
group.append(heading, list);
|
|
5593
|
+
return group;
|
|
5594
|
+
}
|
|
5595
|
+
|
|
5286
5596
|
function renderQueue(event) {
|
|
5287
5597
|
const snapshot = normalizeQueuedMessages(event);
|
|
5288
5598
|
const tabId = event?.tabId || activeTabId;
|
|
5289
5599
|
if (tabId) latestQueuedMessagesByTab.set(tabId, snapshot);
|
|
5290
5600
|
const steering = snapshot.steering;
|
|
5291
5601
|
const followUp = snapshot.followUp;
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5602
|
+
const total = queueMessageCount(snapshot);
|
|
5603
|
+
updateQueueHeaderBadge(total);
|
|
5604
|
+
elements.queueBox.replaceChildren();
|
|
5605
|
+
elements.queueBox.classList.toggle("muted", total === 0);
|
|
5606
|
+
elements.queueBox.classList.toggle("has-items", total > 0);
|
|
5607
|
+
if (total === 0) {
|
|
5608
|
+
elements.queueBox.append(make("div", "queue-empty", "No queued messages."));
|
|
5609
|
+
updateStickyUserPromptButton();
|
|
5295
5610
|
return;
|
|
5296
5611
|
}
|
|
5297
|
-
|
|
5298
|
-
const
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5612
|
+
|
|
5613
|
+
const summary = make("div", "queue-summary");
|
|
5614
|
+
summary.append(make("strong", undefined, `${total} queued message${total === 1 ? "" : "s"}`));
|
|
5615
|
+
const counts = make("div", "queue-summary-counts");
|
|
5616
|
+
if (steering.length) counts.append(queueSummaryPill("steering", steering.length, "steering"));
|
|
5617
|
+
if (followUp.length) counts.append(queueSummaryPill("follow-up", followUp.length, "follow-up"));
|
|
5618
|
+
summary.append(counts);
|
|
5619
|
+
|
|
5620
|
+
elements.queueBox.append(summary);
|
|
5621
|
+
if (steering.length) elements.queueBox.append(renderQueueGroup("Steering", steering, "steering"));
|
|
5622
|
+
if (followUp.length) elements.queueBox.append(renderQueueGroup("Follow-up", followUp, "follow-up"));
|
|
5623
|
+
elements.queueBox.append(make("div", "queue-hint", "Alt+Up restores this queue snapshot to the composer. RPC queue clearing is pending upstream support."));
|
|
5624
|
+
updateStickyUserPromptButton();
|
|
5625
|
+
}
|
|
5626
|
+
|
|
5627
|
+
function nextQueuedFollowUpPrompt(tabId = activeTabId) {
|
|
5628
|
+
const snapshot = tabId ? latestQueuedMessagesByTab.get(tabId) : null;
|
|
5629
|
+
const next = Array.isArray(snapshot?.followUp) ? snapshot.followUp.find((item) => String(item || "").trim()) : null;
|
|
5630
|
+
return next ? stickyUserPromptPreviewText(next) : "";
|
|
5303
5631
|
}
|
|
5304
5632
|
|
|
5305
5633
|
function queuedMessagesForComposer(tabId = activeTabId) {
|
|
@@ -5324,6 +5652,353 @@ function restoreQueuedMessagesToComposerFromShortcut() {
|
|
|
5324
5652
|
return true;
|
|
5325
5653
|
}
|
|
5326
5654
|
|
|
5655
|
+
function normalizePromptListPrompts(prompts) {
|
|
5656
|
+
return (Array.isArray(prompts) ? prompts : [])
|
|
5657
|
+
.map((prompt) => String(prompt || "").trim())
|
|
5658
|
+
.filter(Boolean);
|
|
5659
|
+
}
|
|
5660
|
+
|
|
5661
|
+
function promptListStorageId() {
|
|
5662
|
+
if (window.crypto?.randomUUID) return window.crypto.randomUUID();
|
|
5663
|
+
return `prompt-list-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
5664
|
+
}
|
|
5665
|
+
|
|
5666
|
+
function promptListFallbackName(prompts) {
|
|
5667
|
+
const first = String(prompts?.[0] || "").replace(/\s+/g, " ").trim();
|
|
5668
|
+
if (!first) return "Untitled prompt list";
|
|
5669
|
+
return first.length > 58 ? `${first.slice(0, 57)}…` : first;
|
|
5670
|
+
}
|
|
5671
|
+
|
|
5672
|
+
function normalizePromptListRecord(record) {
|
|
5673
|
+
const prompts = normalizePromptListPrompts(record?.prompts);
|
|
5674
|
+
if (prompts.length === 0) return null;
|
|
5675
|
+
const id = String(record?.id || "").trim() || promptListStorageId();
|
|
5676
|
+
const now = new Date().toISOString();
|
|
5677
|
+
const name = String(record?.name || "").trim() || promptListFallbackName(prompts);
|
|
5678
|
+
return {
|
|
5679
|
+
id,
|
|
5680
|
+
name,
|
|
5681
|
+
prompts,
|
|
5682
|
+
createdAt: String(record?.createdAt || record?.updatedAt || now),
|
|
5683
|
+
updatedAt: String(record?.updatedAt || now),
|
|
5684
|
+
};
|
|
5685
|
+
}
|
|
5686
|
+
|
|
5687
|
+
function readStoredPromptLists() {
|
|
5688
|
+
try {
|
|
5689
|
+
const raw = JSON.parse(localStorage.getItem(PROMPT_LIST_STORAGE_KEY) || "[]");
|
|
5690
|
+
const records = Array.isArray(raw) ? raw : Object.values(raw || {});
|
|
5691
|
+
return records
|
|
5692
|
+
.map(normalizePromptListRecord)
|
|
5693
|
+
.filter(Boolean)
|
|
5694
|
+
.sort((a, b) => String(b.updatedAt || "").localeCompare(String(a.updatedAt || "")));
|
|
5695
|
+
} catch {
|
|
5696
|
+
return [];
|
|
5697
|
+
}
|
|
5698
|
+
}
|
|
5699
|
+
|
|
5700
|
+
function writeStoredPromptLists(lists) {
|
|
5701
|
+
localStorage.setItem(PROMPT_LIST_STORAGE_KEY, JSON.stringify(lists.map(normalizePromptListRecord).filter(Boolean)));
|
|
5702
|
+
}
|
|
5703
|
+
|
|
5704
|
+
function savedPromptListById(id) {
|
|
5705
|
+
const key = String(id || "");
|
|
5706
|
+
return readStoredPromptLists().find((list) => list.id === key) || null;
|
|
5707
|
+
}
|
|
5708
|
+
|
|
5709
|
+
function deleteStoredPromptList(id) {
|
|
5710
|
+
const key = String(id || "");
|
|
5711
|
+
if (!key) return null;
|
|
5712
|
+
const lists = readStoredPromptLists();
|
|
5713
|
+
const deleted = lists.find((list) => list.id === key) || null;
|
|
5714
|
+
if (!deleted) return null;
|
|
5715
|
+
writeStoredPromptLists(lists.filter((list) => list.id !== key));
|
|
5716
|
+
return deleted;
|
|
5717
|
+
}
|
|
5718
|
+
|
|
5719
|
+
function upsertStoredPromptList(record) {
|
|
5720
|
+
const normalized = normalizePromptListRecord(record);
|
|
5721
|
+
if (!normalized) throw new Error("Prompt list needs at least one prompt.");
|
|
5722
|
+
const existing = readStoredPromptLists().filter((list) => list.id !== normalized.id);
|
|
5723
|
+
writeStoredPromptLists([normalized, ...existing]);
|
|
5724
|
+
return normalized;
|
|
5725
|
+
}
|
|
5726
|
+
|
|
5727
|
+
function promptListEditorValues() {
|
|
5728
|
+
return Array.from(elements.promptListEditorRows?.querySelectorAll("textarea") || [])
|
|
5729
|
+
.map((textarea) => textarea.value || "");
|
|
5730
|
+
}
|
|
5731
|
+
|
|
5732
|
+
function currentPromptListPrompts() {
|
|
5733
|
+
return normalizePromptListPrompts(promptListEditorValues());
|
|
5734
|
+
}
|
|
5735
|
+
|
|
5736
|
+
function setPromptListStatus(message = "", level = "muted") {
|
|
5737
|
+
if (!elements.promptListStatus) return;
|
|
5738
|
+
elements.promptListStatus.textContent = message;
|
|
5739
|
+
elements.promptListStatus.className = `prompt-list-status ${level || "muted"}`.trim();
|
|
5740
|
+
}
|
|
5741
|
+
|
|
5742
|
+
function renderPromptListDialogControls() {
|
|
5743
|
+
const hasPrompts = currentPromptListPrompts().length > 0;
|
|
5744
|
+
if (elements.promptListRunListButton) elements.promptListRunListButton.disabled = promptListRunning || !hasPrompts;
|
|
5745
|
+
if (elements.promptListSaveButton) elements.promptListSaveButton.disabled = promptListRunning || !hasPrompts;
|
|
5746
|
+
if (elements.promptListAddPromptButton) elements.promptListAddPromptButton.disabled = promptListRunning;
|
|
5747
|
+
if (elements.promptListDialogLoadButton) elements.promptListDialogLoadButton.disabled = promptListRunning;
|
|
5748
|
+
if (elements.promptListLoadSelectedButton) elements.promptListLoadSelectedButton.disabled = promptListRunning || !elements.promptListSelect?.value;
|
|
5749
|
+
if (elements.promptListDeleteSelectedButton) elements.promptListDeleteSelectedButton.disabled = promptListRunning || !elements.promptListSelect?.value;
|
|
5750
|
+
}
|
|
5751
|
+
|
|
5752
|
+
function renderPromptListEditor(prompts = [""]) {
|
|
5753
|
+
const values = (Array.isArray(prompts) && prompts.length ? prompts : [""]).map((prompt) => String(prompt || ""));
|
|
5754
|
+
elements.promptListEditorRows?.replaceChildren();
|
|
5755
|
+
values.forEach((value, index) => {
|
|
5756
|
+
const row = make("div", "prompt-list-editor-row");
|
|
5757
|
+
const label = make("label", "prompt-list-editor-label");
|
|
5758
|
+
label.setAttribute("for", `promptListPrompt${index}`);
|
|
5759
|
+
label.textContent = index === 0 ? "Start prompt" : `Follow-up #${index}`;
|
|
5760
|
+
const textarea = make("textarea", "dialog-editor prompt-list-textarea");
|
|
5761
|
+
textarea.id = `promptListPrompt${index}`;
|
|
5762
|
+
textarea.rows = index === 0 ? 4 : 3;
|
|
5763
|
+
textarea.placeholder = index === 0 ? "Prompt that starts the run…" : "Follow-up prompt to queue after the start prompt…";
|
|
5764
|
+
textarea.value = value;
|
|
5765
|
+
textarea.addEventListener("input", () => {
|
|
5766
|
+
setPromptListStatus("");
|
|
5767
|
+
renderPromptListDialogControls();
|
|
5768
|
+
});
|
|
5769
|
+
const header = make("div", "prompt-list-editor-row-header");
|
|
5770
|
+
header.append(label);
|
|
5771
|
+
if (index > 0) {
|
|
5772
|
+
const remove = make("button", "prompt-list-remove-button", "×");
|
|
5773
|
+
remove.type = "button";
|
|
5774
|
+
remove.title = `Remove follow-up #${index}`;
|
|
5775
|
+
remove.setAttribute("aria-label", `Remove follow-up prompt ${index}`);
|
|
5776
|
+
remove.addEventListener("click", () => {
|
|
5777
|
+
const next = promptListEditorValues();
|
|
5778
|
+
next.splice(index, 1);
|
|
5779
|
+
renderPromptListEditor(next.length ? next : [""]);
|
|
5780
|
+
setPromptListStatus("Follow-up removed.", "muted");
|
|
5781
|
+
});
|
|
5782
|
+
header.append(remove);
|
|
5783
|
+
}
|
|
5784
|
+
row.append(header, textarea);
|
|
5785
|
+
elements.promptListEditorRows?.append(row);
|
|
5786
|
+
});
|
|
5787
|
+
renderPromptListDialogControls();
|
|
5788
|
+
}
|
|
5789
|
+
|
|
5790
|
+
function populatePromptListSelect(selectedId = "") {
|
|
5791
|
+
if (!elements.promptListSelect) return;
|
|
5792
|
+
const lists = readStoredPromptLists();
|
|
5793
|
+
elements.promptListSelect.replaceChildren();
|
|
5794
|
+
if (lists.length === 0) {
|
|
5795
|
+
const option = make("option", undefined, "No saved prompt lists");
|
|
5796
|
+
option.value = "";
|
|
5797
|
+
elements.promptListSelect.append(option);
|
|
5798
|
+
elements.promptListSelect.disabled = true;
|
|
5799
|
+
} else {
|
|
5800
|
+
elements.promptListSelect.disabled = false;
|
|
5801
|
+
for (const list of lists) {
|
|
5802
|
+
const option = make("option", undefined, `${list.name} (${list.prompts.length})`);
|
|
5803
|
+
option.value = list.id;
|
|
5804
|
+
elements.promptListSelect.append(option);
|
|
5805
|
+
}
|
|
5806
|
+
elements.promptListSelect.value = selectedId && lists.some((list) => list.id === selectedId) ? selectedId : lists[0].id;
|
|
5807
|
+
}
|
|
5808
|
+
renderPromptListDialogControls();
|
|
5809
|
+
}
|
|
5810
|
+
|
|
5811
|
+
function setPromptListLoadPanelVisible(visible) {
|
|
5812
|
+
if (!elements.promptListLoadPanel) return;
|
|
5813
|
+
elements.promptListLoadPanel.hidden = !visible;
|
|
5814
|
+
if (visible) populatePromptListSelect(elements.promptListSelect?.value || elements.promptListDialog?.dataset.promptListId || "");
|
|
5815
|
+
}
|
|
5816
|
+
|
|
5817
|
+
function loadPromptListIntoEditor(record, { updateLoaded = true } = {}) {
|
|
5818
|
+
const list = normalizePromptListRecord(record);
|
|
5819
|
+
if (!list) return false;
|
|
5820
|
+
if (elements.promptListDialog) elements.promptListDialog.dataset.promptListId = list.id;
|
|
5821
|
+
if (elements.promptListNameInput) elements.promptListNameInput.value = list.name;
|
|
5822
|
+
renderPromptListEditor(list.prompts);
|
|
5823
|
+
setPromptListStatus(`Loaded “${list.name}”.`, "success");
|
|
5824
|
+
if (updateLoaded) setLoadedPromptList(list);
|
|
5825
|
+
return true;
|
|
5826
|
+
}
|
|
5827
|
+
|
|
5828
|
+
function loadSelectedPromptListIntoEditor() {
|
|
5829
|
+
const list = savedPromptListById(elements.promptListSelect?.value);
|
|
5830
|
+
if (!list) {
|
|
5831
|
+
setPromptListStatus("No saved prompt list selected.", "warn");
|
|
5832
|
+
return;
|
|
5833
|
+
}
|
|
5834
|
+
if (loadPromptListIntoEditor(list, { updateLoaded: true })) elements.promptListDialog?.close();
|
|
5835
|
+
}
|
|
5836
|
+
|
|
5837
|
+
function deleteSelectedPromptList() {
|
|
5838
|
+
const list = savedPromptListById(elements.promptListSelect?.value);
|
|
5839
|
+
if (!list) {
|
|
5840
|
+
setPromptListStatus("No saved prompt list selected to delete.", "warn");
|
|
5841
|
+
return;
|
|
5842
|
+
}
|
|
5843
|
+
if (!window.confirm(`Delete prompt list “${list.name}”? This cannot be undone.`)) return;
|
|
5844
|
+
const deleted = deleteStoredPromptList(list.id);
|
|
5845
|
+
if (!deleted) {
|
|
5846
|
+
setPromptListStatus("Prompt list was already deleted.", "warn");
|
|
5847
|
+
populatePromptListSelect();
|
|
5848
|
+
return;
|
|
5849
|
+
}
|
|
5850
|
+
if (loadedPromptList?.id === deleted.id) setLoadedPromptList(null);
|
|
5851
|
+
if (elements.promptListDialog?.dataset.promptListId === deleted.id) {
|
|
5852
|
+
elements.promptListDialog.dataset.promptListId = "";
|
|
5853
|
+
if (elements.promptListNameInput) elements.promptListNameInput.value = "";
|
|
5854
|
+
renderPromptListEditor([""]);
|
|
5855
|
+
}
|
|
5856
|
+
populatePromptListSelect();
|
|
5857
|
+
setPromptListStatus(`Deleted “${deleted.name}”.`, "success");
|
|
5858
|
+
addEvent(`deleted prompt list “${deleted.name}”`);
|
|
5859
|
+
}
|
|
5860
|
+
|
|
5861
|
+
function openPromptListDialog({ mode = "create", list = null } = {}) {
|
|
5862
|
+
const normalized = normalizePromptListRecord(list);
|
|
5863
|
+
if (elements.promptListDialog) elements.promptListDialog.dataset.promptListId = normalized?.id || "";
|
|
5864
|
+
if (elements.promptListDialogTitle) elements.promptListDialogTitle.textContent = mode === "load" ? "Load prompt list" : "Create prompt list";
|
|
5865
|
+
if (elements.promptListNameInput) elements.promptListNameInput.value = normalized?.name || "";
|
|
5866
|
+
renderPromptListEditor(normalized?.prompts || [""]);
|
|
5867
|
+
setPromptListStatus(mode === "load" ? "Choose a saved list, then load or run it." : "");
|
|
5868
|
+
setPromptListLoadPanelVisible(mode === "load");
|
|
5869
|
+
if (!elements.promptListDialog?.open) elements.promptListDialog?.showModal();
|
|
5870
|
+
queueMicrotask(() => {
|
|
5871
|
+
const target = mode === "load" && !normalized ? elements.promptListSelect : elements.promptListEditorRows?.querySelector("textarea");
|
|
5872
|
+
target?.focus();
|
|
5873
|
+
});
|
|
5874
|
+
}
|
|
5875
|
+
|
|
5876
|
+
function displayedPromptListRecord() {
|
|
5877
|
+
const prompts = currentPromptListPrompts();
|
|
5878
|
+
if (prompts.length === 0) return null;
|
|
5879
|
+
const id = String(elements.promptListDialog?.dataset.promptListId || "").trim() || promptListStorageId();
|
|
5880
|
+
const existing = savedPromptListById(id);
|
|
5881
|
+
const now = new Date().toISOString();
|
|
5882
|
+
return {
|
|
5883
|
+
id,
|
|
5884
|
+
name: String(elements.promptListNameInput?.value || "").trim() || promptListFallbackName(prompts),
|
|
5885
|
+
prompts,
|
|
5886
|
+
createdAt: existing?.createdAt || now,
|
|
5887
|
+
updatedAt: now,
|
|
5888
|
+
};
|
|
5889
|
+
}
|
|
5890
|
+
|
|
5891
|
+
function saveDisplayedPromptList() {
|
|
5892
|
+
const record = displayedPromptListRecord();
|
|
5893
|
+
if (!record) {
|
|
5894
|
+
setPromptListStatus("Add at least one prompt before saving.", "warn");
|
|
5895
|
+
return null;
|
|
5896
|
+
}
|
|
5897
|
+
try {
|
|
5898
|
+
const saved = upsertStoredPromptList(record);
|
|
5899
|
+
if (elements.promptListDialog) elements.promptListDialog.dataset.promptListId = saved.id;
|
|
5900
|
+
if (elements.promptListNameInput) elements.promptListNameInput.value = saved.name;
|
|
5901
|
+
populatePromptListSelect(saved.id);
|
|
5902
|
+
setLoadedPromptList(saved);
|
|
5903
|
+
setPromptListStatus(`Saved “${saved.name}”.`, "success");
|
|
5904
|
+
addEvent(`saved prompt list “${saved.name}” with ${saved.prompts.length} prompt${saved.prompts.length === 1 ? "" : "s"}`);
|
|
5905
|
+
return saved;
|
|
5906
|
+
} catch (error) {
|
|
5907
|
+
setPromptListStatus(error.message || "Failed to save prompt list.", "error");
|
|
5908
|
+
addEvent(error.message || "failed to save prompt list", "error");
|
|
5909
|
+
return null;
|
|
5910
|
+
}
|
|
5911
|
+
}
|
|
5912
|
+
|
|
5913
|
+
function setLoadedPromptList(record) {
|
|
5914
|
+
loadedPromptList = normalizePromptListRecord(record);
|
|
5915
|
+
renderLoadedPromptListPreview();
|
|
5916
|
+
}
|
|
5917
|
+
|
|
5918
|
+
function renderLoadedPromptListPreview() {
|
|
5919
|
+
if (elements.runLoadedPromptListButton) elements.runLoadedPromptListButton.disabled = promptListRunning || !loadedPromptList;
|
|
5920
|
+
if (!elements.loadedPromptListBox) return;
|
|
5921
|
+
elements.loadedPromptListBox.replaceChildren();
|
|
5922
|
+
elements.loadedPromptListBox.classList.toggle("muted", !loadedPromptList);
|
|
5923
|
+
if (!loadedPromptList) {
|
|
5924
|
+
elements.loadedPromptListBox.textContent = "No prompt list loaded.";
|
|
5925
|
+
return;
|
|
5926
|
+
}
|
|
5927
|
+
const total = loadedPromptList.prompts.length;
|
|
5928
|
+
const followUps = Math.max(0, total - 1);
|
|
5929
|
+
const summary = make("div", "loaded-prompt-list-summary");
|
|
5930
|
+
summary.append(make("strong", undefined, loadedPromptList.name), make("span", undefined, `${total} prompt${total === 1 ? "" : "s"} · ${followUps} follow-up${followUps === 1 ? "" : "s"}`));
|
|
5931
|
+
const preview = make("ol", "prompt-list-preview");
|
|
5932
|
+
loadedPromptList.prompts.slice(0, 4).forEach((prompt, index) => {
|
|
5933
|
+
const item = make("li", "prompt-list-preview-item");
|
|
5934
|
+
item.append(make("span", "prompt-list-preview-index", index === 0 ? "Start" : `#${index}`), make("span", "prompt-list-preview-text", prompt));
|
|
5935
|
+
preview.append(item);
|
|
5936
|
+
});
|
|
5937
|
+
if (loadedPromptList.prompts.length > 4) {
|
|
5938
|
+
const more = make("li", "prompt-list-preview-more", `+${loadedPromptList.prompts.length - 4} more follow-up prompt${loadedPromptList.prompts.length - 4 === 1 ? "" : "s"}`);
|
|
5939
|
+
preview.append(more);
|
|
5940
|
+
}
|
|
5941
|
+
elements.loadedPromptListBox.append(summary, preview);
|
|
5942
|
+
}
|
|
5943
|
+
|
|
5944
|
+
function setPromptListRunning(running) {
|
|
5945
|
+
promptListRunning = !!running;
|
|
5946
|
+
renderLoadedPromptListPreview();
|
|
5947
|
+
renderPromptListDialogControls();
|
|
5948
|
+
}
|
|
5949
|
+
|
|
5950
|
+
async function runPromptList(prompts, { name = "Prompt list" } = {}) {
|
|
5951
|
+
const listPrompts = normalizePromptListPrompts(prompts);
|
|
5952
|
+
if (listPrompts.length === 0) {
|
|
5953
|
+
setPromptListStatus("Add or load at least one prompt before running.", "warn");
|
|
5954
|
+
return;
|
|
5955
|
+
}
|
|
5956
|
+
const targetTabId = activeTabId;
|
|
5957
|
+
if (!targetTabId) {
|
|
5958
|
+
addEvent("cannot run prompt list without an active tab", "error");
|
|
5959
|
+
setPromptListStatus("No active tab available.", "error");
|
|
5960
|
+
return;
|
|
5961
|
+
}
|
|
5962
|
+
if (promptListRunning) return;
|
|
5963
|
+
const tabContext = activeTabContext(targetTabId);
|
|
5964
|
+
setPromptListRunning(true);
|
|
5965
|
+
setPromptListStatus(`Running “${name}”…`, "muted");
|
|
5966
|
+
addEvent(`running prompt list “${name}” (${listPrompts.length} prompt${listPrompts.length === 1 ? "" : "s"})`);
|
|
5967
|
+
try {
|
|
5968
|
+
await sendPrompt("prompt", listPrompts[0], { targetTabId, throwOnError: true });
|
|
5969
|
+
for (const prompt of listPrompts.slice(1)) {
|
|
5970
|
+
await sendPrompt("follow-up", prompt, { targetTabId, throwOnError: true });
|
|
5971
|
+
}
|
|
5972
|
+
setPromptListStatus(`Queued “${name}”.`, "success");
|
|
5973
|
+
if (isCurrentTabContext(tabContext)) {
|
|
5974
|
+
addEvent(`queued prompt list “${name}”: 1 start prompt and ${Math.max(0, listPrompts.length - 1)} follow-up${listPrompts.length === 2 ? "" : "s"}`);
|
|
5975
|
+
scheduleRefreshState(120, tabContext);
|
|
5976
|
+
}
|
|
5977
|
+
} catch (error) {
|
|
5978
|
+
setPromptListStatus(error.message || "Failed to run prompt list.", "error");
|
|
5979
|
+
addEvent(error.message || "failed to run prompt list", "error");
|
|
5980
|
+
} finally {
|
|
5981
|
+
setPromptListRunning(false);
|
|
5982
|
+
}
|
|
5983
|
+
}
|
|
5984
|
+
|
|
5985
|
+
async function runDisplayedPromptList() {
|
|
5986
|
+
const saved = saveDisplayedPromptList();
|
|
5987
|
+
if (!saved) {
|
|
5988
|
+
setPromptListStatus("Save the list before running so it stays available afterward.", "warn");
|
|
5989
|
+
return;
|
|
5990
|
+
}
|
|
5991
|
+
await runPromptList(saved.prompts, { name: saved.name });
|
|
5992
|
+
}
|
|
5993
|
+
|
|
5994
|
+
async function runLoadedPromptList() {
|
|
5995
|
+
if (!loadedPromptList) {
|
|
5996
|
+
openPromptListDialog({ mode: "load" });
|
|
5997
|
+
return;
|
|
5998
|
+
}
|
|
5999
|
+
await runPromptList(loadedPromptList.prompts, { name: loadedPromptList.name });
|
|
6000
|
+
}
|
|
6001
|
+
|
|
5327
6002
|
function appendText(parent, text, className = "text-block") {
|
|
5328
6003
|
const block = make("pre", className);
|
|
5329
6004
|
block.textContent = text || "";
|
|
@@ -6303,13 +6978,22 @@ function updateStickyUserPromptButton() {
|
|
|
6303
6978
|
button.dataset.compacted = target.compacted ? "true" : "false";
|
|
6304
6979
|
if (Number.isInteger(target.index) && target.index >= 0) button.dataset.messageIndex = String(target.index);
|
|
6305
6980
|
else button.removeAttribute("data-message-index");
|
|
6306
|
-
|
|
6307
|
-
|
|
6308
|
-
|
|
6981
|
+
const nextFollowUp = nextQueuedFollowUpPrompt();
|
|
6982
|
+
const baseTitle = target.compacted ? `Prompt was compacted; jump to compaction summary: ${target.preview}` : `Jump to ${label.toLowerCase()}: ${target.preview}`;
|
|
6983
|
+
const baseAriaLabel = target.compacted ? `Prompt was compacted; jump to compaction summary: ${target.preview}` : `Jump to ${label.toLowerCase()} (${ordinal} of ${targets.length}): ${target.preview}`;
|
|
6984
|
+
button.title = nextFollowUp ? `${baseTitle}\nNext follow-up prompt: ${nextFollowUp}` : baseTitle;
|
|
6985
|
+
button.setAttribute("aria-label", nextFollowUp ? `${baseAriaLabel}. Next follow-up prompt: ${nextFollowUp}` : baseAriaLabel);
|
|
6986
|
+
const children = [
|
|
6309
6987
|
make("span", "sticky-user-prompt-label", label),
|
|
6310
6988
|
make("span", "sticky-user-prompt-text", target.preview),
|
|
6311
6989
|
make("span", "sticky-user-prompt-meta", meta),
|
|
6312
|
-
|
|
6990
|
+
];
|
|
6991
|
+
if (nextFollowUp) {
|
|
6992
|
+
const followUp = make("span", "sticky-user-follow-up-prompt");
|
|
6993
|
+
followUp.append(make("span", "sticky-user-follow-up-label", "Next follow-up"), make("span", "sticky-user-follow-up-text", nextFollowUp));
|
|
6994
|
+
children.push(followUp);
|
|
6995
|
+
}
|
|
6996
|
+
button.replaceChildren(...children);
|
|
6313
6997
|
}
|
|
6314
6998
|
|
|
6315
6999
|
function assistantToolCallId(part) {
|
|
@@ -8071,7 +8755,7 @@ async function openNativeSettingsDialog() {
|
|
|
8071
8755
|
], "How queued follow-ups are delivered after the current response.", { label: "now", tone: "now" }),
|
|
8072
8756
|
transport: nativeSettingSelect("Transport", settings.transport || "auto", SETTINGS_TRANSPORT_OPTIONS, "Preferred provider transport when multiple transports are supported.", { label: "reload", tone: "reload" }),
|
|
8073
8757
|
httpIdleTimeout: nativeSettingSelect("HTTP idle timeout", currentHttpIdleTimeoutValue(settings), httpIdleTimeoutOptions(settings), "Maximum idle gap while waiting for provider HTTP data.", { label: "reload", tone: "reload" }),
|
|
8074
|
-
busyBehavior: nativeSettingSelect("Busy prompt behavior",
|
|
8758
|
+
busyBehavior: nativeSettingSelect("Busy prompt behavior", busyPromptBehavior, [
|
|
8075
8759
|
{ value: "followUp", label: "follow-up" },
|
|
8076
8760
|
{ value: "steer", label: "steer" },
|
|
8077
8761
|
], "When you submit a normal prompt while a tab is already busy.", { label: "browser", tone: "browser" }),
|
|
@@ -8120,7 +8804,7 @@ async function openNativeSettingsDialog() {
|
|
|
8120
8804
|
if (controls.steering.select.value !== (state.steeringMode || "one-at-a-time")) requests.push(nativeCommandApi("/api/steering-mode", { method: "POST", body: { mode: controls.steering.select.value } }));
|
|
8121
8805
|
if (controls.followUp.select.value !== (state.followUpMode || "one-at-a-time")) requests.push(nativeCommandApi("/api/follow-up-mode", { method: "POST", body: { mode: controls.followUp.select.value } }));
|
|
8122
8806
|
if (controls.autoCompact.input.checked !== (state.autoCompactionEnabled !== false)) requests.push(nativeCommandApi("/api/auto-compaction", { method: "POST", body: { enabled: controls.autoCompact.input.checked } }));
|
|
8123
|
-
|
|
8807
|
+
busyPromptBehavior = controls.busyBehavior.select.value;
|
|
8124
8808
|
if (controls.thinkingOutput.input.checked !== thinkingOutputVisible) setThinkingOutputVisible(controls.thinkingOutput.input.checked);
|
|
8125
8809
|
if (controls.doneNotifications.input.checked !== agentDoneNotificationsEnabled) await setAgentDoneNotificationsEnabled(controls.doneNotifications.input.checked);
|
|
8126
8810
|
await Promise.all(requests);
|
|
@@ -9825,11 +10509,10 @@ async function sendUserBashCommand(parsed, { usesPromptInput = false, targetTabI
|
|
|
9825
10509
|
await runUserBashCommand(parsed, { usesPromptInput, targetTabId });
|
|
9826
10510
|
}
|
|
9827
10511
|
|
|
9828
|
-
async function sendPrompt(kind = "prompt", explicitMessage) {
|
|
10512
|
+
async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = activeTabId, throwOnError = false } = {}) {
|
|
9829
10513
|
const usesPromptInput = explicitMessage === undefined;
|
|
9830
10514
|
const rawMessage = usesPromptInput ? elements.promptInput.value : explicitMessage;
|
|
9831
10515
|
const originalMessage = String(rawMessage || "").trim();
|
|
9832
|
-
const targetTabId = activeTabId;
|
|
9833
10516
|
if (!targetTabId) return;
|
|
9834
10517
|
const tabContext = activeTabContext(targetTabId);
|
|
9835
10518
|
const attachments = usesPromptInput ? [...attachmentsForTab(targetTabId)] : [];
|
|
@@ -9842,7 +10525,7 @@ async function sendPrompt(kind = "prompt", explicitMessage) {
|
|
|
9842
10525
|
}
|
|
9843
10526
|
|
|
9844
10527
|
const targetWasStreaming = !!currentState?.isStreaming;
|
|
9845
|
-
const busyBehavior =
|
|
10528
|
+
const busyBehavior = busyPromptBehavior || "followUp";
|
|
9846
10529
|
const startsRun = kind === "prompt" && !targetWasStreaming;
|
|
9847
10530
|
autoFollowChat = true;
|
|
9848
10531
|
updateJumpToLatestButton();
|
|
@@ -9923,6 +10606,7 @@ async function sendPrompt(kind = "prompt", explicitMessage) {
|
|
|
9923
10606
|
addEvent(error.message, "error");
|
|
9924
10607
|
addTransientMessage({ role: "error", title: message.startsWith("/") ? message.split(/\s+/, 1)[0] : "error", content: error.message, level: "error" });
|
|
9925
10608
|
}
|
|
10609
|
+
if (throwOnError) throw error;
|
|
9926
10610
|
}
|
|
9927
10611
|
}
|
|
9928
10612
|
|
|
@@ -10335,6 +11019,25 @@ function connectEvents(tabContext = activeTabContext()) {
|
|
|
10335
11019
|
elements.copyServerCommandButton?.addEventListener("click", copyServerStartCommand);
|
|
10336
11020
|
elements.retryServerConnectionButton?.addEventListener("click", retryServerConnection);
|
|
10337
11021
|
elements.commandSearchInput?.addEventListener("input", renderCommands);
|
|
11022
|
+
elements.createPromptListButton?.addEventListener("click", () => openPromptListDialog({ mode: "create" }));
|
|
11023
|
+
elements.loadPromptListButton?.addEventListener("click", () => openPromptListDialog({ mode: "load", list: loadedPromptList }));
|
|
11024
|
+
elements.runLoadedPromptListButton?.addEventListener("click", () => runLoadedPromptList());
|
|
11025
|
+
elements.promptListAddPromptButton?.addEventListener("click", () => {
|
|
11026
|
+
const next = promptListEditorValues();
|
|
11027
|
+
next.push("");
|
|
11028
|
+
renderPromptListEditor(next);
|
|
11029
|
+
setPromptListStatus("Added follow-up prompt.", "muted");
|
|
11030
|
+
queueMicrotask(() => elements.promptListEditorRows?.querySelector(".prompt-list-editor-row:last-child textarea")?.focus());
|
|
11031
|
+
});
|
|
11032
|
+
elements.promptListDialogLoadButton?.addEventListener("click", () => setPromptListLoadPanelVisible(elements.promptListLoadPanel?.hidden !== false));
|
|
11033
|
+
elements.promptListLoadSelectedButton?.addEventListener("click", loadSelectedPromptListIntoEditor);
|
|
11034
|
+
elements.promptListDeleteSelectedButton?.addEventListener("click", deleteSelectedPromptList);
|
|
11035
|
+
elements.promptListSelect?.addEventListener("change", renderPromptListDialogControls);
|
|
11036
|
+
elements.promptListNameInput?.addEventListener("input", () => setPromptListStatus(""));
|
|
11037
|
+
elements.promptListSaveButton?.addEventListener("click", saveDisplayedPromptList);
|
|
11038
|
+
elements.promptListRunListButton?.addEventListener("click", () => runDisplayedPromptList());
|
|
11039
|
+
elements.promptListCloseButton?.addEventListener("click", () => elements.promptListDialog?.close());
|
|
11040
|
+
elements.promptListDialog?.querySelector("form")?.addEventListener("submit", (event) => event.preventDefault());
|
|
10338
11041
|
elements.sendFeedbackButton.addEventListener("click", () => submitQueuedActionFeedback());
|
|
10339
11042
|
elements.composer.addEventListener("submit", (event) => {
|
|
10340
11043
|
event.preventDefault();
|
|
@@ -10783,6 +11486,21 @@ window.addEventListener("keydown", (event) => {
|
|
|
10783
11486
|
elements.refreshCodexUsageButton?.addEventListener("click", () => {
|
|
10784
11487
|
refreshCodexUsage({ forceAuthRefresh: true }).finally(() => scheduleRefreshCodexUsage());
|
|
10785
11488
|
});
|
|
11489
|
+
elements.pathPickerCreateNameInput.addEventListener("input", updateCreateDirectoryControls);
|
|
11490
|
+
elements.pathPickerCreateNameInput.addEventListener("keydown", (event) => {
|
|
11491
|
+
if (event.key !== "Enter") return;
|
|
11492
|
+
event.preventDefault();
|
|
11493
|
+
createPathPickerDirectory().catch((error) => addEvent(error.message, "error"));
|
|
11494
|
+
});
|
|
11495
|
+
elements.pathPickerCreateButton.addEventListener("click", () => createPathPickerDirectory().catch((error) => addEvent(error.message, "error")));
|
|
11496
|
+
elements.pathPickerSearchInput.addEventListener("input", renderPathPickerDirectoryList);
|
|
11497
|
+
elements.pathPickerSearchInput.addEventListener("keydown", (event) => {
|
|
11498
|
+
if (event.key !== "Enter") return;
|
|
11499
|
+
event.preventDefault();
|
|
11500
|
+
const onlyMatch = pathPickerState?.filteredDirectories?.length === 1 ? pathPickerState.filteredDirectories[0] : null;
|
|
11501
|
+
if (onlyMatch) loadPathPickerDirectory(onlyMatch.cwd);
|
|
11502
|
+
});
|
|
11503
|
+
elements.pathPickerClearSearchButton.addEventListener("click", () => clearPathPickerSearch({ focus: true }));
|
|
10786
11504
|
elements.pathPickerAddFastPickButton.addEventListener("click", () => addCurrentFastPick().catch((error) => addEvent(error.message, "error")));
|
|
10787
11505
|
elements.pathPickerCancelButton.addEventListener("click", () => closePathPicker(null));
|
|
10788
11506
|
elements.pathPickerChooseButton.addEventListener("click", () => closePathPicker(pathPickerState?.cwd || null));
|
|
@@ -10876,6 +11594,7 @@ resizePromptInput();
|
|
|
10876
11594
|
focusPromptInput({ defer: true });
|
|
10877
11595
|
updateComposerModeButtons();
|
|
10878
11596
|
updateOptionalFeatureAvailability();
|
|
11597
|
+
renderLoadedPromptListPreview();
|
|
10879
11598
|
loadLastUserPromptCache();
|
|
10880
11599
|
loadPromptHistoryCache();
|
|
10881
11600
|
installViewportHandlers();
|