@firstpick/pi-package-webui 0.3.3 → 0.3.5
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 +118 -0
- package/package.json +1 -1
- package/public/app.js +608 -3
- package/public/index.html +45 -2
- package/public/styles.css +307 -0
- package/tests/mobile-static.test.mjs +50 -1
package/public/app.js
CHANGED
|
@@ -31,6 +31,16 @@ const elements = {
|
|
|
31
31
|
composerActionsButton: $("#composerActionsButton"),
|
|
32
32
|
composerActionsPanel: $("#composerActionsPanel"),
|
|
33
33
|
promptInput: $("#promptInput"),
|
|
34
|
+
busyPromptBehaviorTag: $("#busyPromptBehaviorTag"),
|
|
35
|
+
busyPromptBehaviorMenu: $("#busyPromptBehaviorMenu"),
|
|
36
|
+
sessionSkillTags: $("#sessionSkillTags"),
|
|
37
|
+
skillEditorDialog: $("#skillEditorDialog"),
|
|
38
|
+
skillEditorTitle: $("#skillEditorTitle"),
|
|
39
|
+
skillEditorMeta: $("#skillEditorMeta"),
|
|
40
|
+
skillEditorText: $("#skillEditorText"),
|
|
41
|
+
skillEditorStatus: $("#skillEditorStatus"),
|
|
42
|
+
skillEditorCancelButton: $("#skillEditorCancelButton"),
|
|
43
|
+
skillEditorSaveButton: $("#skillEditorSaveButton"),
|
|
34
44
|
sendButton: $("#sendButton"),
|
|
35
45
|
commandSuggest: $("#commandSuggest"),
|
|
36
46
|
attachmentTray: $("#attachmentTray"),
|
|
@@ -83,6 +93,8 @@ const elements = {
|
|
|
83
93
|
setThinkingButton: $("#setThinkingButton"),
|
|
84
94
|
thinkingVisibilityToggle: $("#thinkingVisibilityToggle"),
|
|
85
95
|
thinkingVisibilityStatus: $("#thinkingVisibilityStatus"),
|
|
96
|
+
terminalTabsLayoutSelect: $("#terminalTabsLayoutSelect"),
|
|
97
|
+
terminalTabsLayoutStatus: $("#terminalTabsLayoutStatus"),
|
|
86
98
|
themeSelect: $("#themeSelect"),
|
|
87
99
|
backgroundInput: $("#backgroundInput"),
|
|
88
100
|
backgroundChooseButton: $("#backgroundChooseButton"),
|
|
@@ -172,6 +184,7 @@ let activeTabGeneration = 0;
|
|
|
172
184
|
let tabDrafts = new Map();
|
|
173
185
|
let tabAttachments = new Map();
|
|
174
186
|
let activeTextAttachmentEditor = null;
|
|
187
|
+
let activeSkillEditor = null;
|
|
175
188
|
let tabActivities = new Map();
|
|
176
189
|
let tabSeenCompletionSerials = new Map();
|
|
177
190
|
let streamBubble = null;
|
|
@@ -211,6 +224,8 @@ let openTerminalTabGroupKey = null;
|
|
|
211
224
|
let newTabMenuOpen = false;
|
|
212
225
|
let nativeCommandMenuOpen = false;
|
|
213
226
|
let appRunnerMenuOpen = false;
|
|
227
|
+
let busyPromptBehaviorMenuOpen = false;
|
|
228
|
+
const skillUsageByTab = new Map();
|
|
214
229
|
let appRunnerCustomDraft = { id: "", label: "", command: "./", path: "", args: "" };
|
|
215
230
|
let appRunnerFileBrowserState = { open: false, loading: false, path: "", data: null, error: "" };
|
|
216
231
|
let optionsMenuOpen = false;
|
|
@@ -251,6 +266,7 @@ let blockedTabNotificationPermissionRequested = false;
|
|
|
251
266
|
let blockedTabNotificationFallbackNoted = false;
|
|
252
267
|
let agentDoneNotificationsEnabled = false;
|
|
253
268
|
let thinkingOutputVisible = true;
|
|
269
|
+
let terminalTabsLayout = "top";
|
|
254
270
|
let webuiSettings = {};
|
|
255
271
|
let busyPromptBehavior = "followUp";
|
|
256
272
|
let autocompleteMaxVisible = 12;
|
|
@@ -296,6 +312,9 @@ const TAB_STORAGE_KEY = "pi-webui-active-tab";
|
|
|
296
312
|
const PATH_FAST_PICKS_STORAGE_KEY = "pi-webui-path-fast-picks";
|
|
297
313
|
const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications";
|
|
298
314
|
const THINKING_VISIBILITY_STORAGE_KEY = "pi-webui-thinking-visible";
|
|
315
|
+
const BUSY_PROMPT_BEHAVIOR_STORAGE_KEY = "pi-webui-busy-prompt-behavior";
|
|
316
|
+
const SKILL_USAGE_STORAGE_KEY = "pi-webui-skill-usage-v1";
|
|
317
|
+
const TERMINAL_TABS_LAYOUT_STORAGE_KEY = "pi-webui-terminal-tabs-layout";
|
|
299
318
|
const TOOL_OUTPUT_EXPANDED_STORAGE_KEY = "pi-webui-tool-output-expanded";
|
|
300
319
|
const THEME_STORAGE_KEY = "pi-webui-theme";
|
|
301
320
|
const CUSTOM_BACKGROUND_STORAGE_KEY = "pi-webui-custom-background";
|
|
@@ -325,6 +344,12 @@ const LONG_INPUT_ATTACHMENT_MIME_TYPE = "text/plain";
|
|
|
325
344
|
const INLINE_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
326
345
|
const BACKGROUND_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
327
346
|
const DEFAULT_THEME_NAME = "catppuccin-mocha";
|
|
347
|
+
const TERMINAL_TABS_LAYOUTS = new Set(["top", "left"]);
|
|
348
|
+
const TERMINAL_TABS_LAYOUT_LABELS = { top: "Top bar", left: "Left sidebar" };
|
|
349
|
+
const BUSY_PROMPT_BEHAVIOR_VALUES = new Set(["followUp", "steer"]);
|
|
350
|
+
const BUSY_PROMPT_BEHAVIOR_LABELS = { followUp: "Follow-up", steer: "Steer" };
|
|
351
|
+
const SKILL_TAG_MAX_VISIBLE = 6;
|
|
352
|
+
const SKILL_USAGE_LIMIT_PER_TAB = 32;
|
|
328
353
|
const MOBILE_VIEW_QUERY = "(max-width: 720px), (max-device-width: 720px), (pointer: coarse) and (hover: none)";
|
|
329
354
|
const SIDE_PANEL_OVERLAY_QUERY = "(max-width: 1050px), (max-device-width: 720px), (pointer: coarse) and (hover: none)";
|
|
330
355
|
const CHAT_BOTTOM_THRESHOLD_PX = 96;
|
|
@@ -789,6 +814,26 @@ function persistThinkingOutputVisible(visible) {
|
|
|
789
814
|
}
|
|
790
815
|
}
|
|
791
816
|
|
|
817
|
+
function normalizeTerminalTabsLayout(value) {
|
|
818
|
+
return TERMINAL_TABS_LAYOUTS.has(value) ? value : "top";
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
function readStoredTerminalTabsLayout() {
|
|
822
|
+
try {
|
|
823
|
+
return normalizeTerminalTabsLayout(localStorage.getItem(TERMINAL_TABS_LAYOUT_STORAGE_KEY));
|
|
824
|
+
} catch {
|
|
825
|
+
return "top";
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
function persistTerminalTabsLayout(layout) {
|
|
830
|
+
try {
|
|
831
|
+
localStorage.setItem(TERMINAL_TABS_LAYOUT_STORAGE_KEY, normalizeTerminalTabsLayout(layout));
|
|
832
|
+
} catch {
|
|
833
|
+
// Ignore storage failures; the layout control should still work for this page load.
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
|
|
792
837
|
function readStoredToolOutputExpanded() {
|
|
793
838
|
try {
|
|
794
839
|
return localStorage.getItem(TOOL_OUTPUT_EXPANDED_STORAGE_KEY) === "1";
|
|
@@ -816,6 +861,30 @@ function renderThinkingVisibilityToggle() {
|
|
|
816
861
|
if (elements.thinkingVisibilityStatus) elements.thinkingVisibilityStatus.textContent = thinkingVisibilityStatusText();
|
|
817
862
|
}
|
|
818
863
|
|
|
864
|
+
function terminalTabsLayoutStatusText(layout = terminalTabsLayout) {
|
|
865
|
+
return TERMINAL_TABS_LAYOUT_LABELS[normalizeTerminalTabsLayout(layout)] || TERMINAL_TABS_LAYOUT_LABELS.top;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function renderTerminalTabsLayoutControl() {
|
|
869
|
+
const layout = normalizeTerminalTabsLayout(terminalTabsLayout);
|
|
870
|
+
if (elements.terminalTabsLayoutSelect) elements.terminalTabsLayoutSelect.value = layout;
|
|
871
|
+
if (elements.terminalTabsLayoutStatus) elements.terminalTabsLayoutStatus.textContent = terminalTabsLayoutStatusText(layout);
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function setTerminalTabsLayout(layout, { persist = true, announce = false } = {}) {
|
|
875
|
+
const next = normalizeTerminalTabsLayout(layout);
|
|
876
|
+
terminalTabsLayout = next;
|
|
877
|
+
document.body.classList.toggle("terminal-tabs-left", next === "left");
|
|
878
|
+
if (next === "left" && mobileTabsExpanded) setMobileTabsExpanded(false);
|
|
879
|
+
if (persist) persistTerminalTabsLayout(next);
|
|
880
|
+
renderTerminalTabsLayoutControl();
|
|
881
|
+
if (announce) addEvent(`terminal tabs layout changed to ${terminalTabsLayoutStatusText(next).toLowerCase()}`);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function restoreTerminalTabsLayoutSetting() {
|
|
885
|
+
setTerminalTabsLayout(readStoredTerminalTabsLayout(), { persist: false });
|
|
886
|
+
}
|
|
887
|
+
|
|
819
888
|
function removeStreamingThinkingBubble() {
|
|
820
889
|
streamThinkingBubble?.remove();
|
|
821
890
|
streamThinkingBubble = null;
|
|
@@ -855,6 +924,454 @@ function restoreThinkingVisibilitySetting() {
|
|
|
855
924
|
renderThinkingVisibilityToggle();
|
|
856
925
|
}
|
|
857
926
|
|
|
927
|
+
function normalizeBusyPromptBehavior(value) {
|
|
928
|
+
const normalized = String(value || "").trim();
|
|
929
|
+
if (normalized === "follow-up" || normalized.toLowerCase() === "followup") return "followUp";
|
|
930
|
+
return BUSY_PROMPT_BEHAVIOR_VALUES.has(normalized) ? normalized : "followUp";
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function readStoredBusyPromptBehavior() {
|
|
934
|
+
try {
|
|
935
|
+
return normalizeBusyPromptBehavior(localStorage.getItem(BUSY_PROMPT_BEHAVIOR_STORAGE_KEY));
|
|
936
|
+
} catch {
|
|
937
|
+
return "followUp";
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function persistBusyPromptBehavior(behavior) {
|
|
942
|
+
try {
|
|
943
|
+
localStorage.setItem(BUSY_PROMPT_BEHAVIOR_STORAGE_KEY, normalizeBusyPromptBehavior(behavior));
|
|
944
|
+
} catch {
|
|
945
|
+
// Ignore storage failures; the setting should still work for this page load.
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function busyPromptBehaviorMenuItems() {
|
|
950
|
+
return Array.from(elements.busyPromptBehaviorMenu?.querySelectorAll("[data-busy-prompt-behavior]") || []);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function renderBusyPromptBehaviorMenu() {
|
|
954
|
+
const behavior = normalizeBusyPromptBehavior(busyPromptBehavior);
|
|
955
|
+
for (const item of busyPromptBehaviorMenuItems()) {
|
|
956
|
+
const checked = normalizeBusyPromptBehavior(item.dataset.busyPromptBehavior) === behavior;
|
|
957
|
+
item.setAttribute("aria-checked", checked ? "true" : "false");
|
|
958
|
+
item.classList.toggle("active", checked);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function normalizeSkillName(value) {
|
|
963
|
+
const raw = String(value || "").trim().replace(/^\/?skill:/i, "");
|
|
964
|
+
if (!raw) return "";
|
|
965
|
+
const match = raw.match(/^[a-z0-9][a-z0-9._-]{0,63}$/i);
|
|
966
|
+
return match ? match[0].toLowerCase() : "";
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function skillUsageMapForTab(tabId = activeTabId, { create = true } = {}) {
|
|
970
|
+
if (!tabId) return null;
|
|
971
|
+
let map = skillUsageByTab.get(tabId);
|
|
972
|
+
if (!map && create) {
|
|
973
|
+
map = new Map();
|
|
974
|
+
skillUsageByTab.set(tabId, map);
|
|
975
|
+
}
|
|
976
|
+
return map || null;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function clearSkillUsageForTab(tabId = activeTabId) {
|
|
980
|
+
if (!tabId) return;
|
|
981
|
+
skillUsageByTab.delete(tabId);
|
|
982
|
+
persistSkillUsage();
|
|
983
|
+
if (tabId === activeTabId) renderSessionSkillTags(tabId);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
function sortedSkillUsageEntries(tabId = activeTabId) {
|
|
987
|
+
const map = skillUsageMapForTab(tabId, { create: false });
|
|
988
|
+
if (!map) return [];
|
|
989
|
+
return [...map.values()].sort((a, b) => b.lastSeenAt - a.lastSeenAt || a.name.localeCompare(b.name));
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function serializeSkillUsageEntry(entry) {
|
|
993
|
+
const name = normalizeSkillName(entry?.name || "");
|
|
994
|
+
if (!name) return null;
|
|
995
|
+
const kinds = entry?.kinds instanceof Set ? [...entry.kinds] : Array.isArray(entry?.kinds) ? entry.kinds : [];
|
|
996
|
+
const paths = entry?.paths instanceof Set ? [...entry.paths] : Array.isArray(entry?.paths) ? entry.paths : [];
|
|
997
|
+
const sources = entry?.sources instanceof Set ? [...entry.sources] : Array.isArray(entry?.sources) ? entry.sources : [];
|
|
998
|
+
const path = entry?.path || paths[paths.length - 1] || "";
|
|
999
|
+
return {
|
|
1000
|
+
name,
|
|
1001
|
+
firstSeenAt: Number.isFinite(entry?.firstSeenAt) ? entry.firstSeenAt : Date.now(),
|
|
1002
|
+
lastSeenAt: Number.isFinite(entry?.lastSeenAt) ? entry.lastSeenAt : Date.now(),
|
|
1003
|
+
kinds: kinds.includes("read") ? kinds : [...kinds, "read"],
|
|
1004
|
+
sources: sources.slice(-12),
|
|
1005
|
+
path,
|
|
1006
|
+
paths: [...new Set([path, ...paths].filter(Boolean))].slice(-8),
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
function persistSkillUsage() {
|
|
1011
|
+
try {
|
|
1012
|
+
const storedTabs = {};
|
|
1013
|
+
for (const [tabId, map] of skillUsageByTab.entries()) {
|
|
1014
|
+
const entries = [...map.values()]
|
|
1015
|
+
.filter((entry) => entry?.kinds?.has("read"))
|
|
1016
|
+
.map(serializeSkillUsageEntry)
|
|
1017
|
+
.filter(Boolean)
|
|
1018
|
+
.slice(0, SKILL_USAGE_LIMIT_PER_TAB);
|
|
1019
|
+
if (entries.length) storedTabs[tabId] = entries;
|
|
1020
|
+
}
|
|
1021
|
+
localStorage.setItem(SKILL_USAGE_STORAGE_KEY, JSON.stringify({ version: 1, tabs: storedTabs }));
|
|
1022
|
+
} catch {
|
|
1023
|
+
// Ignore storage failures; tags still work for the current page lifetime.
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function restoreStoredSkillUsage() {
|
|
1028
|
+
try {
|
|
1029
|
+
const parsed = JSON.parse(localStorage.getItem(SKILL_USAGE_STORAGE_KEY) || "{}");
|
|
1030
|
+
const storedTabs = parsed?.tabs && typeof parsed.tabs === "object" ? parsed.tabs : {};
|
|
1031
|
+
for (const [tabId, entries] of Object.entries(storedTabs)) {
|
|
1032
|
+
if (!tabId || !Array.isArray(entries)) continue;
|
|
1033
|
+
const map = skillUsageMapForTab(tabId);
|
|
1034
|
+
if (!map) continue;
|
|
1035
|
+
for (const stored of entries.slice(0, SKILL_USAGE_LIMIT_PER_TAB)) {
|
|
1036
|
+
const name = normalizeSkillName(stored?.name || "");
|
|
1037
|
+
if (!name) continue;
|
|
1038
|
+
const kinds = new Set(Array.isArray(stored?.kinds) ? stored.kinds : ["read"]);
|
|
1039
|
+
if (!kinds.has("read")) continue;
|
|
1040
|
+
const paths = new Set(Array.isArray(stored?.paths) ? stored.paths.filter(Boolean) : []);
|
|
1041
|
+
if (stored?.path) paths.add(stored.path);
|
|
1042
|
+
map.set(name, {
|
|
1043
|
+
name,
|
|
1044
|
+
firstSeenAt: Number.isFinite(stored?.firstSeenAt) ? stored.firstSeenAt : Date.now(),
|
|
1045
|
+
lastSeenAt: Number.isFinite(stored?.lastSeenAt) ? stored.lastSeenAt : Date.now(),
|
|
1046
|
+
kinds,
|
|
1047
|
+
sources: new Set(Array.isArray(stored?.sources) ? stored.sources : ["stored"]),
|
|
1048
|
+
path: stored?.path || [...paths].at(-1) || "",
|
|
1049
|
+
paths,
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
} catch {
|
|
1054
|
+
// Ignore corrupt stored tag data.
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function pruneSkillUsageForKnownTabs(tabIds) {
|
|
1059
|
+
let changed = false;
|
|
1060
|
+
for (const tabId of skillUsageByTab.keys()) {
|
|
1061
|
+
if (tabIds.has(tabId)) continue;
|
|
1062
|
+
skillUsageByTab.delete(tabId);
|
|
1063
|
+
changed = true;
|
|
1064
|
+
}
|
|
1065
|
+
if (changed) persistSkillUsage();
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function skillInfoFromPath(pathText) {
|
|
1069
|
+
const normalized = String(pathText || "").trim().replace(/\\/g, "/");
|
|
1070
|
+
const match = normalized.match(/\/skills\/([^/]+)\/SKILL\.md$/i);
|
|
1071
|
+
const name = normalizeSkillName(match?.[1] || "");
|
|
1072
|
+
return name ? { name, path: normalized } : null;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
function skillNameFromPath(pathText) {
|
|
1076
|
+
return skillInfoFromPath(pathText)?.name || "";
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function skillNamesFromSlashCommands(text) {
|
|
1080
|
+
const names = new Set();
|
|
1081
|
+
for (const match of String(text || "").matchAll(/\/skill:([a-z0-9][a-z0-9._-]{0,63})/gi)) {
|
|
1082
|
+
const normalized = normalizeSkillName(match[1]);
|
|
1083
|
+
if (normalized) names.add(normalized);
|
|
1084
|
+
}
|
|
1085
|
+
return [...names];
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function skillKindsLabel(entry) {
|
|
1089
|
+
return entry?.kinds?.has("read") ? "context read" : "tracked";
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function renderSessionSkillTags(tabId = activeTabId) {
|
|
1093
|
+
const container = elements.sessionSkillTags;
|
|
1094
|
+
if (!container) return;
|
|
1095
|
+
const entries = sortedSkillUsageEntries(tabId).filter((entry) => entry.kinds.has("read"));
|
|
1096
|
+
container.replaceChildren();
|
|
1097
|
+
if (!entries.length) {
|
|
1098
|
+
container.hidden = true;
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
const visible = entries.slice(0, SKILL_TAG_MAX_VISIBLE);
|
|
1102
|
+
for (const entry of visible) {
|
|
1103
|
+
const classes = ["composer-skill-tag", "read"];
|
|
1104
|
+
const tag = make("button", classes.join(" "), entry.name);
|
|
1105
|
+
tag.type = "button";
|
|
1106
|
+
tag.dataset.skillName = entry.name;
|
|
1107
|
+
tag.dataset.skillPath = skillPathForEntry(entry);
|
|
1108
|
+
tag.title = `Open and edit skill ${entry.name} (${skillKindsLabel(entry)}) tracked in this tab/session.`;
|
|
1109
|
+
tag.setAttribute("aria-label", `Open skill ${entry.name}`);
|
|
1110
|
+
tag.addEventListener("click", () => openSkillEditor(entry));
|
|
1111
|
+
container.append(tag);
|
|
1112
|
+
}
|
|
1113
|
+
if (entries.length > visible.length) {
|
|
1114
|
+
const overflow = make("span", "composer-skill-tag overflow", `+${entries.length - visible.length}`);
|
|
1115
|
+
overflow.title = `${entries.length - visible.length} more tracked skill${entries.length - visible.length === 1 ? "" : "s"}.`;
|
|
1116
|
+
container.append(overflow);
|
|
1117
|
+
}
|
|
1118
|
+
container.hidden = false;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function trackSkillUsage(tabId, skillName, kind = "used", source = "", details = {}) {
|
|
1122
|
+
const name = normalizeSkillName(skillName);
|
|
1123
|
+
if (!tabId || !name) return;
|
|
1124
|
+
const map = skillUsageMapForTab(tabId);
|
|
1125
|
+
if (!map) return;
|
|
1126
|
+
const now = Date.now();
|
|
1127
|
+
const entry = map.get(name) || { name, firstSeenAt: now, lastSeenAt: now, kinds: new Set(), sources: new Set(), paths: new Set() };
|
|
1128
|
+
entry.lastSeenAt = now;
|
|
1129
|
+
if (["used", "loaded", "read"].includes(kind)) entry.kinds.add(kind);
|
|
1130
|
+
else entry.kinds.add("used");
|
|
1131
|
+
if (source) entry.sources.add(source);
|
|
1132
|
+
if (details?.path) {
|
|
1133
|
+
entry.path = details.path;
|
|
1134
|
+
entry.paths ||= new Set();
|
|
1135
|
+
entry.paths.add(details.path);
|
|
1136
|
+
}
|
|
1137
|
+
map.set(name, entry);
|
|
1138
|
+
if (map.size > SKILL_USAGE_LIMIT_PER_TAB) {
|
|
1139
|
+
const keep = [...map.values()].sort((a, b) => b.lastSeenAt - a.lastSeenAt).slice(0, SKILL_USAGE_LIMIT_PER_TAB);
|
|
1140
|
+
map.clear();
|
|
1141
|
+
for (const item of keep) map.set(item.name, item);
|
|
1142
|
+
}
|
|
1143
|
+
persistSkillUsage();
|
|
1144
|
+
if (tabId === activeTabId) renderSessionSkillTags(tabId);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
function trackSkillsFromText(tabId, text, { kind = "used", source = "" } = {}) {
|
|
1148
|
+
// Intentionally do not tag /skill:name mentions. A skill tag means the
|
|
1149
|
+
// agent read that skill's full SKILL.md context, not only its command/name.
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function trackSkillsFromValue(tabId, value, { keyHint = "", kind = "used", source = "", depth = 0 } = {}) {
|
|
1153
|
+
if (!tabId || value === undefined || value === null || depth > 5) return;
|
|
1154
|
+
if (Array.isArray(value)) {
|
|
1155
|
+
for (const item of value) trackSkillsFromValue(tabId, item, { keyHint, kind, source, depth: depth + 1 });
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
if (typeof value === "string") {
|
|
1159
|
+
const skillInfo = skillInfoFromPath(value);
|
|
1160
|
+
if (skillInfo) trackSkillUsage(tabId, skillInfo.name, "read", source, { path: skillInfo.path });
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (typeof value !== "object") return;
|
|
1164
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
1165
|
+
const hint = String(key || "").toLowerCase();
|
|
1166
|
+
trackSkillsFromValue(tabId, nested, { keyHint: hint, kind, source, depth: depth + 1 });
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function trackSkillsFromToolInvocation(tabId, toolName, args, { sourcePrefix = "tool" } = {}) {
|
|
1171
|
+
if (!tabId) return;
|
|
1172
|
+
const name = String(toolName || "").trim();
|
|
1173
|
+
if (name.toLowerCase() !== "read") return;
|
|
1174
|
+
const source = `${sourcePrefix}:${name}`;
|
|
1175
|
+
trackSkillsFromValue(tabId, args, { kind: "read", source });
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
function trackSkillsFromMessage(tabId, message) {
|
|
1179
|
+
if (!tabId || !message) return;
|
|
1180
|
+
const role = String(message.role || "");
|
|
1181
|
+
if (role === "toolExecution" || role === "toolCall") {
|
|
1182
|
+
trackSkillsFromToolInvocation(tabId, message.toolName || message.name, message.arguments ?? message.args ?? {}, { sourcePrefix: `message:${role}` });
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
if (role === "user" || role === "assistant" || role === "assistantEvent" || role === "native") {
|
|
1186
|
+
trackSkillsFromText(tabId, textFromContent(message.content), { kind: "used", source: `message:${role}` });
|
|
1187
|
+
return;
|
|
1188
|
+
}
|
|
1189
|
+
if (role === "bashExecution") {
|
|
1190
|
+
trackSkillsFromText(tabId, `${message.command || ""}\n${message.output || ""}`, { kind: "used", source: "message:bash" });
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function trackSkillsFromMessages(messages = latestMessages, tabId = activeTabId) {
|
|
1195
|
+
for (const message of messages || []) trackSkillsFromMessage(tabId, message);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function trackSkillsFromEvent(event) {
|
|
1199
|
+
const tabId = event?.tabId || activeTabId;
|
|
1200
|
+
if (!tabId || !event) return;
|
|
1201
|
+
if (["tool_execution_start", "tool_execution_update", "tool_execution_end"].includes(event.type)) {
|
|
1202
|
+
trackSkillsFromToolInvocation(tabId, event.toolName, event.args, { sourcePrefix: `event:${event.type}` });
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
if (event.type === "message_update") {
|
|
1206
|
+
const update = event.assistantMessageEvent || {};
|
|
1207
|
+
if (update.type === "toolcall_start") {
|
|
1208
|
+
trackSkillsFromToolInvocation(tabId, update.name || update.toolName || update.toolCall?.name, update.arguments || update.args || update.toolCall?.arguments || {}, { sourcePrefix: "event:message_update" });
|
|
1209
|
+
}
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
if (event.type === "response" && event.command === "new_session") {
|
|
1213
|
+
clearSkillUsageForTab(tabId);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
function skillPathForEntry(entry) {
|
|
1218
|
+
if (entry?.path) return entry.path;
|
|
1219
|
+
if (entry?.paths instanceof Set && entry.paths.size) {
|
|
1220
|
+
const paths = [...entry.paths];
|
|
1221
|
+
return paths[paths.length - 1] || "";
|
|
1222
|
+
}
|
|
1223
|
+
return "";
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
function skillEditorApiPath({ name = "", path = "" } = {}) {
|
|
1227
|
+
const params = new URLSearchParams();
|
|
1228
|
+
if (name) params.set("name", name);
|
|
1229
|
+
if (path) params.set("path", path);
|
|
1230
|
+
const query = params.toString();
|
|
1231
|
+
return query ? `/api/skill-file?${query}` : "/api/skill-file";
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function setSkillEditorStatus(message = "", level = "muted") {
|
|
1235
|
+
const status = elements.skillEditorStatus;
|
|
1236
|
+
if (!status) return;
|
|
1237
|
+
status.textContent = message;
|
|
1238
|
+
status.className = `skill-editor-status ${level || "muted"}`;
|
|
1239
|
+
status.hidden = !message;
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
function closeSkillEditor() {
|
|
1243
|
+
if (elements.skillEditorDialog?.open) elements.skillEditorDialog.close();
|
|
1244
|
+
else activeSkillEditor = null;
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function updateSkillEditorMeta(data = activeSkillEditor || {}) {
|
|
1248
|
+
if (!elements.skillEditorMeta) return;
|
|
1249
|
+
const parts = [data.name ? `Skill: ${data.name}` : "Skill", data.path || "path unavailable"].filter(Boolean);
|
|
1250
|
+
elements.skillEditorMeta.textContent = parts.join(" · ");
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
async function openSkillEditor(entry) {
|
|
1254
|
+
const name = normalizeSkillName(entry?.name || "");
|
|
1255
|
+
const path = skillPathForEntry(entry);
|
|
1256
|
+
if (!name || !elements.skillEditorDialog || !elements.skillEditorText) return;
|
|
1257
|
+
const tabId = activeTabId;
|
|
1258
|
+
activeSkillEditor = { name, path, tabId, mtimeMs: null };
|
|
1259
|
+
if (elements.skillEditorTitle) elements.skillEditorTitle.textContent = `Edit skill: ${name}`;
|
|
1260
|
+
if (elements.skillEditorText) elements.skillEditorText.value = "";
|
|
1261
|
+
if (elements.skillEditorSaveButton) elements.skillEditorSaveButton.disabled = true;
|
|
1262
|
+
updateSkillEditorMeta(activeSkillEditor);
|
|
1263
|
+
setSkillEditorStatus("Loading skill context…", "muted");
|
|
1264
|
+
if (!elements.skillEditorDialog.open) elements.skillEditorDialog.showModal();
|
|
1265
|
+
|
|
1266
|
+
try {
|
|
1267
|
+
const response = await api(skillEditorApiPath({ name, path }), { tabId });
|
|
1268
|
+
if (activeSkillEditor?.tabId !== tabId || activeSkillEditor?.name !== name) return;
|
|
1269
|
+
const data = response.data || {};
|
|
1270
|
+
activeSkillEditor = { name: normalizeSkillName(data.name || name), path: data.path || path, tabId, mtimeMs: data.mtimeMs };
|
|
1271
|
+
if (elements.skillEditorTitle) elements.skillEditorTitle.textContent = `Edit skill: ${activeSkillEditor.name}`;
|
|
1272
|
+
if (elements.skillEditorText) elements.skillEditorText.value = data.content || "";
|
|
1273
|
+
if (elements.skillEditorSaveButton) elements.skillEditorSaveButton.disabled = false;
|
|
1274
|
+
updateSkillEditorMeta(activeSkillEditor);
|
|
1275
|
+
setSkillEditorStatus("Edit this SKILL.md, then save. Reload the tab if title/description metadata should refresh immediately.", "muted");
|
|
1276
|
+
queueMicrotask(() => elements.skillEditorText?.focus());
|
|
1277
|
+
} catch (error) {
|
|
1278
|
+
if (elements.skillEditorSaveButton) elements.skillEditorSaveButton.disabled = true;
|
|
1279
|
+
setSkillEditorStatus(`Failed to open skill: ${error.message || String(error)}`, "error");
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
async function saveSkillEditor() {
|
|
1284
|
+
if (!activeSkillEditor || !elements.skillEditorText || !elements.skillEditorSaveButton) return;
|
|
1285
|
+
const editor = activeSkillEditor;
|
|
1286
|
+
const previousLabel = elements.skillEditorSaveButton.textContent;
|
|
1287
|
+
elements.skillEditorSaveButton.disabled = true;
|
|
1288
|
+
elements.skillEditorSaveButton.textContent = "Saving…";
|
|
1289
|
+
setSkillEditorStatus("Saving skill…", "muted");
|
|
1290
|
+
try {
|
|
1291
|
+
const response = await api("/api/skill-file", {
|
|
1292
|
+
method: "POST",
|
|
1293
|
+
tabId: editor.tabId,
|
|
1294
|
+
body: {
|
|
1295
|
+
name: editor.name,
|
|
1296
|
+
path: editor.path,
|
|
1297
|
+
mtimeMs: editor.mtimeMs,
|
|
1298
|
+
content: elements.skillEditorText.value,
|
|
1299
|
+
},
|
|
1300
|
+
});
|
|
1301
|
+
const data = response.data || {};
|
|
1302
|
+
const savedName = normalizeSkillName(data.name || editor.name);
|
|
1303
|
+
activeSkillEditor = { name: savedName, path: data.path || editor.path, tabId: editor.tabId, mtimeMs: data.mtimeMs };
|
|
1304
|
+
const map = skillUsageMapForTab(editor.tabId, { create: false });
|
|
1305
|
+
if (map && savedName !== editor.name) map.delete(editor.name);
|
|
1306
|
+
trackSkillUsage(editor.tabId, savedName, "read", "skill-editor", { path: activeSkillEditor.path });
|
|
1307
|
+
if (elements.skillEditorTitle) elements.skillEditorTitle.textContent = `Edit skill: ${savedName}`;
|
|
1308
|
+
updateSkillEditorMeta(activeSkillEditor);
|
|
1309
|
+
setSkillEditorStatus("Saved SKILL.md. Reload/restart affected tabs before relying on updated skill metadata or newly loaded instructions.", "ok");
|
|
1310
|
+
} catch (error) {
|
|
1311
|
+
setSkillEditorStatus(`Failed to save skill: ${error.message || String(error)}`, "error");
|
|
1312
|
+
} finally {
|
|
1313
|
+
elements.skillEditorSaveButton.textContent = previousLabel || "Save skill";
|
|
1314
|
+
elements.skillEditorSaveButton.disabled = false;
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function renderBusyPromptBehaviorTag() {
|
|
1319
|
+
const tag = elements.busyPromptBehaviorTag;
|
|
1320
|
+
if (!tag) return;
|
|
1321
|
+
const behavior = normalizeBusyPromptBehavior(busyPromptBehavior);
|
|
1322
|
+
const label = BUSY_PROMPT_BEHAVIOR_LABELS[behavior] || BUSY_PROMPT_BEHAVIOR_LABELS.followUp;
|
|
1323
|
+
tag.textContent = label;
|
|
1324
|
+
tag.classList.toggle("follow-up", behavior === "followUp");
|
|
1325
|
+
tag.classList.toggle("steer", behavior === "steer");
|
|
1326
|
+
tag.title = behavior === "steer"
|
|
1327
|
+
? "While Pi is running, normal prompt submit steers the active run. Click to change."
|
|
1328
|
+
: "While Pi is running, normal prompt submit queues a follow-up. Click to change.";
|
|
1329
|
+
tag.setAttribute("aria-label", tag.title);
|
|
1330
|
+
renderBusyPromptBehaviorMenu();
|
|
1331
|
+
renderSessionSkillTags(activeTabId);
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
function setBusyPromptBehaviorMenuOpen(open, { focusCurrent = false } = {}) {
|
|
1335
|
+
busyPromptBehaviorMenuOpen = !!open;
|
|
1336
|
+
elements.busyPromptBehaviorTag?.setAttribute("aria-expanded", busyPromptBehaviorMenuOpen ? "true" : "false");
|
|
1337
|
+
elements.busyPromptBehaviorTag?.classList.toggle("menu-open", busyPromptBehaviorMenuOpen);
|
|
1338
|
+
if (elements.busyPromptBehaviorMenu) elements.busyPromptBehaviorMenu.hidden = !busyPromptBehaviorMenuOpen;
|
|
1339
|
+
if (!busyPromptBehaviorMenuOpen) return;
|
|
1340
|
+
renderBusyPromptBehaviorMenu();
|
|
1341
|
+
if (focusCurrent) {
|
|
1342
|
+
requestAnimationFrame(() => {
|
|
1343
|
+
const current = busyPromptBehaviorMenuItems().find((item) => item.getAttribute("aria-checked") === "true") || busyPromptBehaviorMenuItems()[0];
|
|
1344
|
+
current?.focus({ preventScroll: true });
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function focusBusyPromptBehaviorMenuItem(direction = 1) {
|
|
1350
|
+
const items = busyPromptBehaviorMenuItems();
|
|
1351
|
+
if (!items.length) return;
|
|
1352
|
+
const currentIndex = Math.max(0, items.indexOf(document.activeElement));
|
|
1353
|
+
const nextIndex = (currentIndex + direction + items.length) % items.length;
|
|
1354
|
+
items[nextIndex].focus({ preventScroll: true });
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function chooseBusyPromptBehaviorFromMenu(value) {
|
|
1358
|
+
setBusyPromptBehavior(value);
|
|
1359
|
+
setBusyPromptBehaviorMenuOpen(false);
|
|
1360
|
+
focusPromptInput({ defer: true });
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function setBusyPromptBehavior(value, { persist = true } = {}) {
|
|
1364
|
+
const next = normalizeBusyPromptBehavior(value);
|
|
1365
|
+
busyPromptBehavior = next;
|
|
1366
|
+
webuiSettings = { ...webuiSettings, busyPromptBehavior: next };
|
|
1367
|
+
if (persist) persistBusyPromptBehavior(next);
|
|
1368
|
+
renderBusyPromptBehaviorTag();
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function restoreBusyPromptBehaviorSetting() {
|
|
1372
|
+
setBusyPromptBehavior(readStoredBusyPromptBehavior(), { persist: false });
|
|
1373
|
+
}
|
|
1374
|
+
|
|
858
1375
|
function clampAutocompleteMaxVisible(value) {
|
|
859
1376
|
const number = Number(value);
|
|
860
1377
|
if (!Number.isFinite(number)) return 12;
|
|
@@ -867,6 +1384,7 @@ function applyNativeSettingsForBrowser(settings = {}, { syncThinkingVisibility =
|
|
|
867
1384
|
if (settings.autocompleteMaxVisible !== undefined) autocompleteMaxVisible = clampAutocompleteMaxVisible(settings.autocompleteMaxVisible);
|
|
868
1385
|
if (SETTINGS_DOUBLE_ESCAPE_OPTIONS.some((option) => option.value === settings.doubleEscapeAction)) doubleEscapeAction = settings.doubleEscapeAction;
|
|
869
1386
|
if (SETTINGS_TREE_FILTER_OPTIONS.includes(settings.treeFilterMode)) treeFilterMode = settings.treeFilterMode;
|
|
1387
|
+
if (BUSY_PROMPT_BEHAVIOR_VALUES.has(settings.busyPromptBehavior)) setBusyPromptBehavior(settings.busyPromptBehavior);
|
|
870
1388
|
if (syncThinkingVisibility && typeof settings.hideThinkingBlock === "boolean") setThinkingOutputVisible(!settings.hideThinkingBlock);
|
|
871
1389
|
}
|
|
872
1390
|
|
|
@@ -886,6 +1404,7 @@ function setComposerActionsOpen(open) {
|
|
|
886
1404
|
setNativeCommandMenuOpen(false);
|
|
887
1405
|
setAppRunnerMenuOpen(false);
|
|
888
1406
|
setOptionsMenuOpen(false);
|
|
1407
|
+
setBusyPromptBehaviorMenuOpen(false);
|
|
889
1408
|
}
|
|
890
1409
|
}
|
|
891
1410
|
|
|
@@ -943,6 +1462,7 @@ function updateComposerModeButtons() {
|
|
|
943
1462
|
elements.abortButton.textContent = abortRequestInFlight ? "Aborting…" : "Abort";
|
|
944
1463
|
elements.abortButton.title = abortAvailable ? "Abort the active Pi run (Esc or hold)" : "Abort is available while Pi is running";
|
|
945
1464
|
elements.abortButton.setAttribute("aria-label", elements.abortButton.title);
|
|
1465
|
+
renderBusyPromptBehaviorTag();
|
|
946
1466
|
document.body.classList.toggle("pi-run-active", runActive || abortAvailable);
|
|
947
1467
|
}
|
|
948
1468
|
|
|
@@ -2667,9 +3187,11 @@ function syncTabMetadata(nextTabs = []) {
|
|
|
2667
3187
|
tabActivities.delete(tabId);
|
|
2668
3188
|
tabSeenCompletionSerials.delete(tabId);
|
|
2669
3189
|
actionFeedbackByTab.delete(tabId);
|
|
3190
|
+
skillUsageByTab.delete(tabId);
|
|
2670
3191
|
clearGitWorkflowForTab(tabId);
|
|
2671
3192
|
}
|
|
2672
3193
|
}
|
|
3194
|
+
pruneSkillUsageForKnownTabs(liveIds);
|
|
2673
3195
|
}
|
|
2674
3196
|
|
|
2675
3197
|
function applyTabMetadata(tab) {
|
|
@@ -2876,7 +3398,7 @@ function restoreActiveDraft() {
|
|
|
2876
3398
|
|
|
2877
3399
|
function focusPromptInput({ defer = false } = {}) {
|
|
2878
3400
|
const focus = () => {
|
|
2879
|
-
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || elements.nativeCommandDialog.open || elements.appRunnerInfoDialog?.open || elements.promptListDialog?.open || elements.attachmentTextDialog?.open || document.visibilityState === "hidden") return;
|
|
3401
|
+
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || elements.nativeCommandDialog.open || elements.appRunnerInfoDialog?.open || elements.promptListDialog?.open || elements.attachmentTextDialog?.open || elements.skillEditorDialog?.open || document.visibilityState === "hidden") return;
|
|
2880
3402
|
try {
|
|
2881
3403
|
elements.promptInput.focus({ preventScroll: true });
|
|
2882
3404
|
} catch {
|
|
@@ -3231,6 +3753,7 @@ async function refreshTabs({ selectStored = false } = {}) {
|
|
|
3231
3753
|
setActiveTabId((requested && tabs.some((tab) => tab.id === requested) ? requested : stored && tabs.some((tab) => tab.id === stored) ? stored : tabs[0]?.id) || null, { remember: true });
|
|
3232
3754
|
}
|
|
3233
3755
|
rememberServerStartCwd(tabs.find((tab) => tab.id === activeTabId)?.cwd || tabs[0]?.cwd);
|
|
3756
|
+
renderSessionSkillTags(activeTabId);
|
|
3234
3757
|
renderTabs();
|
|
3235
3758
|
return tabs;
|
|
3236
3759
|
}
|
|
@@ -10155,7 +10678,7 @@ async function openNativeSettingsDialog() {
|
|
|
10155
10678
|
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 } }));
|
|
10156
10679
|
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 } }));
|
|
10157
10680
|
if (controls.autoCompact.input.checked !== (state.autoCompactionEnabled !== false)) requests.push(nativeCommandApi("/api/auto-compaction", { method: "POST", body: { enabled: controls.autoCompact.input.checked } }));
|
|
10158
|
-
|
|
10681
|
+
setBusyPromptBehavior(controls.busyBehavior.select.value);
|
|
10159
10682
|
if (controls.thinkingOutput.input.checked !== thinkingOutputVisible) setThinkingOutputVisible(controls.thinkingOutput.input.checked);
|
|
10160
10683
|
if (controls.doneNotifications.input.checked !== agentDoneNotificationsEnabled) await setAgentDoneNotificationsEnabled(controls.doneNotifications.input.checked);
|
|
10161
10684
|
await Promise.all(requests);
|
|
@@ -10628,6 +11151,7 @@ function renderMessages(messages) {
|
|
|
10628
11151
|
cleanupLiveToolRunsForMessages(latestMessages);
|
|
10629
11152
|
syncLastUserPromptFromMessages(latestMessages);
|
|
10630
11153
|
syncPromptHistoryFromMessages(latestMessages);
|
|
11154
|
+
trackSkillsFromMessages(latestMessages, activeTabId);
|
|
10631
11155
|
renderAllMessages();
|
|
10632
11156
|
renderFooter();
|
|
10633
11157
|
renderFeedbackTray();
|
|
@@ -11877,7 +12401,7 @@ async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = acti
|
|
|
11877
12401
|
}
|
|
11878
12402
|
|
|
11879
12403
|
const targetWasStreaming = !!currentState?.isStreaming;
|
|
11880
|
-
const busyBehavior = busyPromptBehavior
|
|
12404
|
+
const busyBehavior = normalizeBusyPromptBehavior(busyPromptBehavior);
|
|
11881
12405
|
const startsRun = kind === "prompt" && !targetWasStreaming;
|
|
11882
12406
|
autoFollowChat = true;
|
|
11883
12407
|
updateJumpToLatestButton();
|
|
@@ -12138,6 +12662,7 @@ function handleInactiveTabEvent(event) {
|
|
|
12138
12662
|
|
|
12139
12663
|
function handleEvent(event) {
|
|
12140
12664
|
ingestEventTabActivity(event);
|
|
12665
|
+
trackSkillsFromEvent(event);
|
|
12141
12666
|
if (!eventTargetsActiveTab(event)) {
|
|
12142
12667
|
handleInactiveTabEvent(event);
|
|
12143
12668
|
return;
|
|
@@ -12435,6 +12960,20 @@ elements.attachmentTextDialog?.addEventListener("keydown", (event) => {
|
|
|
12435
12960
|
if (!elements.attachmentTextSaveButton?.disabled) saveTextAttachmentEdit();
|
|
12436
12961
|
});
|
|
12437
12962
|
elements.attachmentTextDialog?.querySelector("form")?.addEventListener("submit", (event) => event.preventDefault());
|
|
12963
|
+
elements.skillEditorCancelButton?.addEventListener("click", closeSkillEditor);
|
|
12964
|
+
elements.skillEditorSaveButton?.addEventListener("click", saveSkillEditor);
|
|
12965
|
+
elements.skillEditorText?.addEventListener("input", () => setSkillEditorStatus("Unsaved skill edits.", "warn"));
|
|
12966
|
+
elements.skillEditorDialog?.addEventListener("close", () => {
|
|
12967
|
+
activeSkillEditor = null;
|
|
12968
|
+
if (elements.skillEditorText) elements.skillEditorText.value = "";
|
|
12969
|
+
setSkillEditorStatus("");
|
|
12970
|
+
});
|
|
12971
|
+
elements.skillEditorDialog?.addEventListener("keydown", (event) => {
|
|
12972
|
+
if (!(event.ctrlKey || event.metaKey) || event.altKey || event.shiftKey || event.key.toLowerCase() !== "s") return;
|
|
12973
|
+
event.preventDefault();
|
|
12974
|
+
if (!elements.skillEditorSaveButton?.disabled) saveSkillEditor();
|
|
12975
|
+
});
|
|
12976
|
+
elements.skillEditorDialog?.querySelector("form")?.addEventListener("submit", (event) => event.preventDefault());
|
|
12438
12977
|
elements.sendFeedbackButton.addEventListener("click", () => submitQueuedActionFeedback());
|
|
12439
12978
|
elements.composer.addEventListener("submit", (event) => {
|
|
12440
12979
|
event.preventDefault();
|
|
@@ -12443,6 +12982,56 @@ elements.composer.addEventListener("submit", (event) => {
|
|
|
12443
12982
|
elements.composerActionsButton.addEventListener("click", () => {
|
|
12444
12983
|
setComposerActionsOpen(!document.body.classList.contains("composer-actions-open"));
|
|
12445
12984
|
});
|
|
12985
|
+
elements.busyPromptBehaviorTag?.addEventListener("click", (event) => {
|
|
12986
|
+
event.preventDefault();
|
|
12987
|
+
const nextOpen = !busyPromptBehaviorMenuOpen;
|
|
12988
|
+
setPublishMenuOpen(false);
|
|
12989
|
+
setNativeCommandMenuOpen(false);
|
|
12990
|
+
setAppRunnerMenuOpen(false);
|
|
12991
|
+
setOptionsMenuOpen(false);
|
|
12992
|
+
setComposerActionsOpen(false);
|
|
12993
|
+
setBusyPromptBehaviorMenuOpen(nextOpen);
|
|
12994
|
+
});
|
|
12995
|
+
elements.busyPromptBehaviorTag?.addEventListener("keydown", (event) => {
|
|
12996
|
+
if (!["ArrowDown", "ArrowUp", "Enter", " "].includes(event.key)) return;
|
|
12997
|
+
event.preventDefault();
|
|
12998
|
+
const focusPrevious = event.key === "ArrowUp";
|
|
12999
|
+
setBusyPromptBehaviorMenuOpen(true);
|
|
13000
|
+
requestAnimationFrame(() => {
|
|
13001
|
+
const items = busyPromptBehaviorMenuItems();
|
|
13002
|
+
if (!items.length) return;
|
|
13003
|
+
const currentIndex = Math.max(0, items.findIndex((item) => item.getAttribute("aria-checked") === "true"));
|
|
13004
|
+
const targetIndex = focusPrevious ? (currentIndex - 1 + items.length) % items.length : currentIndex;
|
|
13005
|
+
items[targetIndex]?.focus({ preventScroll: true });
|
|
13006
|
+
});
|
|
13007
|
+
});
|
|
13008
|
+
elements.busyPromptBehaviorMenu?.addEventListener("click", (event) => {
|
|
13009
|
+
const item = event.target?.closest?.("[data-busy-prompt-behavior]");
|
|
13010
|
+
if (!item) return;
|
|
13011
|
+
chooseBusyPromptBehaviorFromMenu(item.dataset.busyPromptBehavior);
|
|
13012
|
+
});
|
|
13013
|
+
elements.busyPromptBehaviorMenu?.addEventListener("keydown", (event) => {
|
|
13014
|
+
if (event.key === "Escape") {
|
|
13015
|
+
event.preventDefault();
|
|
13016
|
+
setBusyPromptBehaviorMenuOpen(false);
|
|
13017
|
+
elements.busyPromptBehaviorTag?.focus({ preventScroll: true });
|
|
13018
|
+
return;
|
|
13019
|
+
}
|
|
13020
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
13021
|
+
event.preventDefault();
|
|
13022
|
+
focusBusyPromptBehaviorMenuItem(event.key === "ArrowDown" ? 1 : -1);
|
|
13023
|
+
return;
|
|
13024
|
+
}
|
|
13025
|
+
if (event.key === "Home" || event.key === "End") {
|
|
13026
|
+
event.preventDefault();
|
|
13027
|
+
const items = busyPromptBehaviorMenuItems();
|
|
13028
|
+
items[event.key === "Home" ? 0 : items.length - 1]?.focus({ preventScroll: true });
|
|
13029
|
+
return;
|
|
13030
|
+
}
|
|
13031
|
+
if (event.key === "Tab") {
|
|
13032
|
+
setBusyPromptBehaviorMenuOpen(false);
|
|
13033
|
+
}
|
|
13034
|
+
});
|
|
12446
13035
|
elements.steerButton.addEventListener("click", () => sendPromptFromModeButton("steer", elements.steerButton));
|
|
12447
13036
|
elements.followUpButton.addEventListener("click", () => sendPromptFromModeButton("follow-up", elements.followUpButton));
|
|
12448
13037
|
elements.terminalTabsToggleButton.addEventListener("click", () => {
|
|
@@ -12831,6 +13420,11 @@ if (elements.thinkingVisibilityToggle) {
|
|
|
12831
13420
|
setThinkingOutputVisible(elements.thinkingVisibilityToggle.checked, { announce: true });
|
|
12832
13421
|
});
|
|
12833
13422
|
}
|
|
13423
|
+
if (elements.terminalTabsLayoutSelect) {
|
|
13424
|
+
elements.terminalTabsLayoutSelect.addEventListener("change", () => {
|
|
13425
|
+
setTerminalTabsLayout(elements.terminalTabsLayoutSelect.value, { announce: true });
|
|
13426
|
+
});
|
|
13427
|
+
}
|
|
12834
13428
|
elements.toggleSidePanelButton.addEventListener("click", () => {
|
|
12835
13429
|
setSidePanelCollapsed(true);
|
|
12836
13430
|
});
|
|
@@ -12874,6 +13468,9 @@ document.addEventListener("pointerdown", (event) => {
|
|
|
12874
13468
|
if (optionsMenuOpen && !event.target?.closest?.(".composer-options-menu")) {
|
|
12875
13469
|
setOptionsMenuOpen(false);
|
|
12876
13470
|
}
|
|
13471
|
+
if (busyPromptBehaviorMenuOpen && !event.target?.closest?.(".composer-context-tags, .composer-busy-mode-menu")) {
|
|
13472
|
+
setBusyPromptBehaviorMenuOpen(false);
|
|
13473
|
+
}
|
|
12877
13474
|
if (document.body.classList.contains("mobile-tabs-expanded") && !elements.tabBar.contains(event.target) && !elements.terminalTabsToggleButton.contains(event.target)) {
|
|
12878
13475
|
setNewTabMenuOpen(false);
|
|
12879
13476
|
setMobileTabsExpanded(false);
|
|
@@ -12974,6 +13571,11 @@ window.addEventListener("keydown", (event) => {
|
|
|
12974
13571
|
setOptionsMenuOpen(false);
|
|
12975
13572
|
return;
|
|
12976
13573
|
}
|
|
13574
|
+
if (busyPromptBehaviorMenuOpen) {
|
|
13575
|
+
setBusyPromptBehaviorMenuOpen(false);
|
|
13576
|
+
elements.busyPromptBehaviorTag?.focus({ preventScroll: true });
|
|
13577
|
+
return;
|
|
13578
|
+
}
|
|
12977
13579
|
if (newTabMenuOpen) {
|
|
12978
13580
|
setNewTabMenuOpen(false);
|
|
12979
13581
|
return;
|
|
@@ -13126,6 +13728,8 @@ elements.promptInput.addEventListener("blur", () => {
|
|
|
13126
13728
|
|
|
13127
13729
|
resizePromptInput();
|
|
13128
13730
|
focusPromptInput({ defer: true });
|
|
13731
|
+
restoreStoredSkillUsage();
|
|
13732
|
+
restoreBusyPromptBehaviorSetting();
|
|
13129
13733
|
updateComposerModeButtons();
|
|
13130
13734
|
updateOptionalFeatureAvailability();
|
|
13131
13735
|
renderAppRunnerControls();
|
|
@@ -13142,6 +13746,7 @@ initializeThemes().catch((error) => {
|
|
|
13142
13746
|
initializeFastPicks().catch((error) => addEvent(`failed to initialize path fast picks: ${error.message}`, "error"));
|
|
13143
13747
|
restoreAgentDoneNotificationsSetting();
|
|
13144
13748
|
restoreThinkingVisibilitySetting();
|
|
13749
|
+
restoreTerminalTabsLayoutSetting();
|
|
13145
13750
|
restoreToolOutputExpansionSetting();
|
|
13146
13751
|
restoreSidePanelSectionState();
|
|
13147
13752
|
bindSidePanelSectionToggles();
|