@firstpick/pi-package-webui 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/bin/pi-webui.mjs +360 -0
- package/package.json +1 -1
- package/public/app.js +738 -5
- package/public/index.html +52 -3
- package/public/styles.css +354 -35
- package/tests/mobile-static.test.mjs +59 -4
package/public/app.js
CHANGED
|
@@ -15,6 +15,12 @@ const elements = {
|
|
|
15
15
|
serverOfflinePanel: $("#serverOfflinePanel"),
|
|
16
16
|
serverRestartPanel: $("#serverRestartPanel"),
|
|
17
17
|
serverRestartMessage: $("#serverRestartMessage"),
|
|
18
|
+
updateNotification: $("#updateNotification"),
|
|
19
|
+
updateNotificationTitle: $("#updateNotificationTitle"),
|
|
20
|
+
updateNotificationMessage: $("#updateNotificationMessage"),
|
|
21
|
+
updateNotificationDetail: $("#updateNotificationDetail"),
|
|
22
|
+
updateNotificationUpdateButton: $("#updateNotificationUpdateButton"),
|
|
23
|
+
updateNotificationDismissButton: $("#updateNotificationDismissButton"),
|
|
18
24
|
serverOfflineCommand: $("#serverOfflineCommand"),
|
|
19
25
|
serverOfflineSlashCommand: $("#serverOfflineSlashCommand"),
|
|
20
26
|
copyServerCommandButton: $("#copyServerCommandButton"),
|
|
@@ -31,6 +37,16 @@ const elements = {
|
|
|
31
37
|
composerActionsButton: $("#composerActionsButton"),
|
|
32
38
|
composerActionsPanel: $("#composerActionsPanel"),
|
|
33
39
|
promptInput: $("#promptInput"),
|
|
40
|
+
busyPromptBehaviorTag: $("#busyPromptBehaviorTag"),
|
|
41
|
+
busyPromptBehaviorMenu: $("#busyPromptBehaviorMenu"),
|
|
42
|
+
sessionSkillTags: $("#sessionSkillTags"),
|
|
43
|
+
skillEditorDialog: $("#skillEditorDialog"),
|
|
44
|
+
skillEditorTitle: $("#skillEditorTitle"),
|
|
45
|
+
skillEditorMeta: $("#skillEditorMeta"),
|
|
46
|
+
skillEditorText: $("#skillEditorText"),
|
|
47
|
+
skillEditorStatus: $("#skillEditorStatus"),
|
|
48
|
+
skillEditorCancelButton: $("#skillEditorCancelButton"),
|
|
49
|
+
skillEditorSaveButton: $("#skillEditorSaveButton"),
|
|
34
50
|
sendButton: $("#sendButton"),
|
|
35
51
|
commandSuggest: $("#commandSuggest"),
|
|
36
52
|
attachmentTray: $("#attachmentTray"),
|
|
@@ -174,6 +190,7 @@ let activeTabGeneration = 0;
|
|
|
174
190
|
let tabDrafts = new Map();
|
|
175
191
|
let tabAttachments = new Map();
|
|
176
192
|
let activeTextAttachmentEditor = null;
|
|
193
|
+
let activeSkillEditor = null;
|
|
177
194
|
let tabActivities = new Map();
|
|
178
195
|
let tabSeenCompletionSerials = new Map();
|
|
179
196
|
let streamBubble = null;
|
|
@@ -213,6 +230,8 @@ let openTerminalTabGroupKey = null;
|
|
|
213
230
|
let newTabMenuOpen = false;
|
|
214
231
|
let nativeCommandMenuOpen = false;
|
|
215
232
|
let appRunnerMenuOpen = false;
|
|
233
|
+
let busyPromptBehaviorMenuOpen = false;
|
|
234
|
+
const skillUsageByTab = new Map();
|
|
216
235
|
let appRunnerCustomDraft = { id: "", label: "", command: "./", path: "", args: "" };
|
|
217
236
|
let appRunnerFileBrowserState = { open: false, loading: false, path: "", data: null, error: "" };
|
|
218
237
|
let optionsMenuOpen = false;
|
|
@@ -238,6 +257,10 @@ let refreshCodexUsageTimer = null;
|
|
|
238
257
|
let codexUsageRenderTimer = null;
|
|
239
258
|
let backendOffline = false;
|
|
240
259
|
let serverRestartInProgress = false;
|
|
260
|
+
let updateRequestInProgress = false;
|
|
261
|
+
let latestUpdateStatus = null;
|
|
262
|
+
let updateStatusRefreshTimer = null;
|
|
263
|
+
let updateNotificationHideTimer = null;
|
|
241
264
|
let backendOfflineNoticeShown = false;
|
|
242
265
|
let latestMessages = [];
|
|
243
266
|
let promptHistoryByTab = new Map();
|
|
@@ -298,7 +321,10 @@ const SIDE_PANEL_SECTION_STORAGE_KEY = "pi-webui-side-panel-sections-collapsed";
|
|
|
298
321
|
const TAB_STORAGE_KEY = "pi-webui-active-tab";
|
|
299
322
|
const PATH_FAST_PICKS_STORAGE_KEY = "pi-webui-path-fast-picks";
|
|
300
323
|
const AGENT_DONE_NOTIFICATIONS_STORAGE_KEY = "pi-webui-agent-done-notifications";
|
|
324
|
+
const UPDATE_NOTIFICATION_DISMISS_STORAGE_KEY = "pi-webui-update-notification-dismissed";
|
|
301
325
|
const THINKING_VISIBILITY_STORAGE_KEY = "pi-webui-thinking-visible";
|
|
326
|
+
const BUSY_PROMPT_BEHAVIOR_STORAGE_KEY = "pi-webui-busy-prompt-behavior";
|
|
327
|
+
const SKILL_USAGE_STORAGE_KEY = "pi-webui-skill-usage-v1";
|
|
302
328
|
const TERMINAL_TABS_LAYOUT_STORAGE_KEY = "pi-webui-terminal-tabs-layout";
|
|
303
329
|
const TOOL_OUTPUT_EXPANDED_STORAGE_KEY = "pi-webui-tool-output-expanded";
|
|
304
330
|
const THEME_STORAGE_KEY = "pi-webui-theme";
|
|
@@ -331,6 +357,10 @@ const BACKGROUND_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/w
|
|
|
331
357
|
const DEFAULT_THEME_NAME = "catppuccin-mocha";
|
|
332
358
|
const TERMINAL_TABS_LAYOUTS = new Set(["top", "left"]);
|
|
333
359
|
const TERMINAL_TABS_LAYOUT_LABELS = { top: "Top bar", left: "Left sidebar" };
|
|
360
|
+
const BUSY_PROMPT_BEHAVIOR_VALUES = new Set(["followUp", "steer"]);
|
|
361
|
+
const BUSY_PROMPT_BEHAVIOR_LABELS = { followUp: "Follow-up", steer: "Steer" };
|
|
362
|
+
const SKILL_TAG_MAX_VISIBLE = 6;
|
|
363
|
+
const SKILL_USAGE_LIMIT_PER_TAB = 32;
|
|
334
364
|
const MOBILE_VIEW_QUERY = "(max-width: 720px), (max-device-width: 720px), (pointer: coarse) and (hover: none)";
|
|
335
365
|
const SIDE_PANEL_OVERLAY_QUERY = "(max-width: 1050px), (max-device-width: 720px), (pointer: coarse) and (hover: none)";
|
|
336
366
|
const CHAT_BOTTOM_THRESHOLD_PX = 96;
|
|
@@ -341,6 +371,8 @@ const CHAT_PROGRAMMATIC_SCROLL_GRACE_MS = 500;
|
|
|
341
371
|
const CHAT_USER_SCROLL_INTENT_MS = 700;
|
|
342
372
|
const CODEX_USAGE_REFRESH_MS = 5 * 60 * 1000;
|
|
343
373
|
const CODEX_USAGE_RENDER_TICK_MS = 30 * 1000;
|
|
374
|
+
const UPDATE_STATUS_REFRESH_MS = 6 * 60 * 60 * 1000;
|
|
375
|
+
const UPDATE_STATUS_INITIAL_DELAY_MS = 1800;
|
|
344
376
|
const RUN_INDICATOR_TICK_MS = 1000;
|
|
345
377
|
const RUN_INDICATOR_START_GRACE_MS = 2500;
|
|
346
378
|
const RUN_INDICATOR_STATE_RECHECK_MS = 5000;
|
|
@@ -905,6 +937,454 @@ function restoreThinkingVisibilitySetting() {
|
|
|
905
937
|
renderThinkingVisibilityToggle();
|
|
906
938
|
}
|
|
907
939
|
|
|
940
|
+
function normalizeBusyPromptBehavior(value) {
|
|
941
|
+
const normalized = String(value || "").trim();
|
|
942
|
+
if (normalized === "follow-up" || normalized.toLowerCase() === "followup") return "followUp";
|
|
943
|
+
return BUSY_PROMPT_BEHAVIOR_VALUES.has(normalized) ? normalized : "followUp";
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function readStoredBusyPromptBehavior() {
|
|
947
|
+
try {
|
|
948
|
+
return normalizeBusyPromptBehavior(localStorage.getItem(BUSY_PROMPT_BEHAVIOR_STORAGE_KEY));
|
|
949
|
+
} catch {
|
|
950
|
+
return "followUp";
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function persistBusyPromptBehavior(behavior) {
|
|
955
|
+
try {
|
|
956
|
+
localStorage.setItem(BUSY_PROMPT_BEHAVIOR_STORAGE_KEY, normalizeBusyPromptBehavior(behavior));
|
|
957
|
+
} catch {
|
|
958
|
+
// Ignore storage failures; the setting should still work for this page load.
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
function busyPromptBehaviorMenuItems() {
|
|
963
|
+
return Array.from(elements.busyPromptBehaviorMenu?.querySelectorAll("[data-busy-prompt-behavior]") || []);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function renderBusyPromptBehaviorMenu() {
|
|
967
|
+
const behavior = normalizeBusyPromptBehavior(busyPromptBehavior);
|
|
968
|
+
for (const item of busyPromptBehaviorMenuItems()) {
|
|
969
|
+
const checked = normalizeBusyPromptBehavior(item.dataset.busyPromptBehavior) === behavior;
|
|
970
|
+
item.setAttribute("aria-checked", checked ? "true" : "false");
|
|
971
|
+
item.classList.toggle("active", checked);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function normalizeSkillName(value) {
|
|
976
|
+
const raw = String(value || "").trim().replace(/^\/?skill:/i, "");
|
|
977
|
+
if (!raw) return "";
|
|
978
|
+
const match = raw.match(/^[a-z0-9][a-z0-9._-]{0,63}$/i);
|
|
979
|
+
return match ? match[0].toLowerCase() : "";
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function skillUsageMapForTab(tabId = activeTabId, { create = true } = {}) {
|
|
983
|
+
if (!tabId) return null;
|
|
984
|
+
let map = skillUsageByTab.get(tabId);
|
|
985
|
+
if (!map && create) {
|
|
986
|
+
map = new Map();
|
|
987
|
+
skillUsageByTab.set(tabId, map);
|
|
988
|
+
}
|
|
989
|
+
return map || null;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function clearSkillUsageForTab(tabId = activeTabId) {
|
|
993
|
+
if (!tabId) return;
|
|
994
|
+
skillUsageByTab.delete(tabId);
|
|
995
|
+
persistSkillUsage();
|
|
996
|
+
if (tabId === activeTabId) renderSessionSkillTags(tabId);
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function sortedSkillUsageEntries(tabId = activeTabId) {
|
|
1000
|
+
const map = skillUsageMapForTab(tabId, { create: false });
|
|
1001
|
+
if (!map) return [];
|
|
1002
|
+
return [...map.values()].sort((a, b) => b.lastSeenAt - a.lastSeenAt || a.name.localeCompare(b.name));
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function serializeSkillUsageEntry(entry) {
|
|
1006
|
+
const name = normalizeSkillName(entry?.name || "");
|
|
1007
|
+
if (!name) return null;
|
|
1008
|
+
const kinds = entry?.kinds instanceof Set ? [...entry.kinds] : Array.isArray(entry?.kinds) ? entry.kinds : [];
|
|
1009
|
+
const paths = entry?.paths instanceof Set ? [...entry.paths] : Array.isArray(entry?.paths) ? entry.paths : [];
|
|
1010
|
+
const sources = entry?.sources instanceof Set ? [...entry.sources] : Array.isArray(entry?.sources) ? entry.sources : [];
|
|
1011
|
+
const path = entry?.path || paths[paths.length - 1] || "";
|
|
1012
|
+
return {
|
|
1013
|
+
name,
|
|
1014
|
+
firstSeenAt: Number.isFinite(entry?.firstSeenAt) ? entry.firstSeenAt : Date.now(),
|
|
1015
|
+
lastSeenAt: Number.isFinite(entry?.lastSeenAt) ? entry.lastSeenAt : Date.now(),
|
|
1016
|
+
kinds: kinds.includes("read") ? kinds : [...kinds, "read"],
|
|
1017
|
+
sources: sources.slice(-12),
|
|
1018
|
+
path,
|
|
1019
|
+
paths: [...new Set([path, ...paths].filter(Boolean))].slice(-8),
|
|
1020
|
+
};
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
function persistSkillUsage() {
|
|
1024
|
+
try {
|
|
1025
|
+
const storedTabs = {};
|
|
1026
|
+
for (const [tabId, map] of skillUsageByTab.entries()) {
|
|
1027
|
+
const entries = [...map.values()]
|
|
1028
|
+
.filter((entry) => entry?.kinds?.has("read"))
|
|
1029
|
+
.map(serializeSkillUsageEntry)
|
|
1030
|
+
.filter(Boolean)
|
|
1031
|
+
.slice(0, SKILL_USAGE_LIMIT_PER_TAB);
|
|
1032
|
+
if (entries.length) storedTabs[tabId] = entries;
|
|
1033
|
+
}
|
|
1034
|
+
localStorage.setItem(SKILL_USAGE_STORAGE_KEY, JSON.stringify({ version: 1, tabs: storedTabs }));
|
|
1035
|
+
} catch {
|
|
1036
|
+
// Ignore storage failures; tags still work for the current page lifetime.
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
function restoreStoredSkillUsage() {
|
|
1041
|
+
try {
|
|
1042
|
+
const parsed = JSON.parse(localStorage.getItem(SKILL_USAGE_STORAGE_KEY) || "{}");
|
|
1043
|
+
const storedTabs = parsed?.tabs && typeof parsed.tabs === "object" ? parsed.tabs : {};
|
|
1044
|
+
for (const [tabId, entries] of Object.entries(storedTabs)) {
|
|
1045
|
+
if (!tabId || !Array.isArray(entries)) continue;
|
|
1046
|
+
const map = skillUsageMapForTab(tabId);
|
|
1047
|
+
if (!map) continue;
|
|
1048
|
+
for (const stored of entries.slice(0, SKILL_USAGE_LIMIT_PER_TAB)) {
|
|
1049
|
+
const name = normalizeSkillName(stored?.name || "");
|
|
1050
|
+
if (!name) continue;
|
|
1051
|
+
const kinds = new Set(Array.isArray(stored?.kinds) ? stored.kinds : ["read"]);
|
|
1052
|
+
if (!kinds.has("read")) continue;
|
|
1053
|
+
const paths = new Set(Array.isArray(stored?.paths) ? stored.paths.filter(Boolean) : []);
|
|
1054
|
+
if (stored?.path) paths.add(stored.path);
|
|
1055
|
+
map.set(name, {
|
|
1056
|
+
name,
|
|
1057
|
+
firstSeenAt: Number.isFinite(stored?.firstSeenAt) ? stored.firstSeenAt : Date.now(),
|
|
1058
|
+
lastSeenAt: Number.isFinite(stored?.lastSeenAt) ? stored.lastSeenAt : Date.now(),
|
|
1059
|
+
kinds,
|
|
1060
|
+
sources: new Set(Array.isArray(stored?.sources) ? stored.sources : ["stored"]),
|
|
1061
|
+
path: stored?.path || [...paths].at(-1) || "",
|
|
1062
|
+
paths,
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
} catch {
|
|
1067
|
+
// Ignore corrupt stored tag data.
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function pruneSkillUsageForKnownTabs(tabIds) {
|
|
1072
|
+
let changed = false;
|
|
1073
|
+
for (const tabId of skillUsageByTab.keys()) {
|
|
1074
|
+
if (tabIds.has(tabId)) continue;
|
|
1075
|
+
skillUsageByTab.delete(tabId);
|
|
1076
|
+
changed = true;
|
|
1077
|
+
}
|
|
1078
|
+
if (changed) persistSkillUsage();
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function skillInfoFromPath(pathText) {
|
|
1082
|
+
const normalized = String(pathText || "").trim().replace(/\\/g, "/");
|
|
1083
|
+
const match = normalized.match(/\/skills\/([^/]+)\/SKILL\.md$/i);
|
|
1084
|
+
const name = normalizeSkillName(match?.[1] || "");
|
|
1085
|
+
return name ? { name, path: normalized } : null;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function skillNameFromPath(pathText) {
|
|
1089
|
+
return skillInfoFromPath(pathText)?.name || "";
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function skillNamesFromSlashCommands(text) {
|
|
1093
|
+
const names = new Set();
|
|
1094
|
+
for (const match of String(text || "").matchAll(/\/skill:([a-z0-9][a-z0-9._-]{0,63})/gi)) {
|
|
1095
|
+
const normalized = normalizeSkillName(match[1]);
|
|
1096
|
+
if (normalized) names.add(normalized);
|
|
1097
|
+
}
|
|
1098
|
+
return [...names];
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function skillKindsLabel(entry) {
|
|
1102
|
+
return entry?.kinds?.has("read") ? "context read" : "tracked";
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function renderSessionSkillTags(tabId = activeTabId) {
|
|
1106
|
+
const container = elements.sessionSkillTags;
|
|
1107
|
+
if (!container) return;
|
|
1108
|
+
const entries = sortedSkillUsageEntries(tabId).filter((entry) => entry.kinds.has("read"));
|
|
1109
|
+
container.replaceChildren();
|
|
1110
|
+
if (!entries.length) {
|
|
1111
|
+
container.hidden = true;
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
const visible = entries.slice(0, SKILL_TAG_MAX_VISIBLE);
|
|
1115
|
+
for (const entry of visible) {
|
|
1116
|
+
const classes = ["composer-skill-tag", "read"];
|
|
1117
|
+
const tag = make("button", classes.join(" "), entry.name);
|
|
1118
|
+
tag.type = "button";
|
|
1119
|
+
tag.dataset.skillName = entry.name;
|
|
1120
|
+
tag.dataset.skillPath = skillPathForEntry(entry);
|
|
1121
|
+
tag.title = `Open and edit skill ${entry.name} (${skillKindsLabel(entry)}) tracked in this tab/session.`;
|
|
1122
|
+
tag.setAttribute("aria-label", `Open skill ${entry.name}`);
|
|
1123
|
+
tag.addEventListener("click", () => openSkillEditor(entry));
|
|
1124
|
+
container.append(tag);
|
|
1125
|
+
}
|
|
1126
|
+
if (entries.length > visible.length) {
|
|
1127
|
+
const overflow = make("span", "composer-skill-tag overflow", `+${entries.length - visible.length}`);
|
|
1128
|
+
overflow.title = `${entries.length - visible.length} more tracked skill${entries.length - visible.length === 1 ? "" : "s"}.`;
|
|
1129
|
+
container.append(overflow);
|
|
1130
|
+
}
|
|
1131
|
+
container.hidden = false;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function trackSkillUsage(tabId, skillName, kind = "used", source = "", details = {}) {
|
|
1135
|
+
const name = normalizeSkillName(skillName);
|
|
1136
|
+
if (!tabId || !name) return;
|
|
1137
|
+
const map = skillUsageMapForTab(tabId);
|
|
1138
|
+
if (!map) return;
|
|
1139
|
+
const now = Date.now();
|
|
1140
|
+
const entry = map.get(name) || { name, firstSeenAt: now, lastSeenAt: now, kinds: new Set(), sources: new Set(), paths: new Set() };
|
|
1141
|
+
entry.lastSeenAt = now;
|
|
1142
|
+
if (["used", "loaded", "read"].includes(kind)) entry.kinds.add(kind);
|
|
1143
|
+
else entry.kinds.add("used");
|
|
1144
|
+
if (source) entry.sources.add(source);
|
|
1145
|
+
if (details?.path) {
|
|
1146
|
+
entry.path = details.path;
|
|
1147
|
+
entry.paths ||= new Set();
|
|
1148
|
+
entry.paths.add(details.path);
|
|
1149
|
+
}
|
|
1150
|
+
map.set(name, entry);
|
|
1151
|
+
if (map.size > SKILL_USAGE_LIMIT_PER_TAB) {
|
|
1152
|
+
const keep = [...map.values()].sort((a, b) => b.lastSeenAt - a.lastSeenAt).slice(0, SKILL_USAGE_LIMIT_PER_TAB);
|
|
1153
|
+
map.clear();
|
|
1154
|
+
for (const item of keep) map.set(item.name, item);
|
|
1155
|
+
}
|
|
1156
|
+
persistSkillUsage();
|
|
1157
|
+
if (tabId === activeTabId) renderSessionSkillTags(tabId);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function trackSkillsFromText(tabId, text, { kind = "used", source = "" } = {}) {
|
|
1161
|
+
// Intentionally do not tag /skill:name mentions. A skill tag means the
|
|
1162
|
+
// agent read that skill's full SKILL.md context, not only its command/name.
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function trackSkillsFromValue(tabId, value, { keyHint = "", kind = "used", source = "", depth = 0 } = {}) {
|
|
1166
|
+
if (!tabId || value === undefined || value === null || depth > 5) return;
|
|
1167
|
+
if (Array.isArray(value)) {
|
|
1168
|
+
for (const item of value) trackSkillsFromValue(tabId, item, { keyHint, kind, source, depth: depth + 1 });
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
if (typeof value === "string") {
|
|
1172
|
+
const skillInfo = skillInfoFromPath(value);
|
|
1173
|
+
if (skillInfo) trackSkillUsage(tabId, skillInfo.name, "read", source, { path: skillInfo.path });
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
if (typeof value !== "object") return;
|
|
1177
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
1178
|
+
const hint = String(key || "").toLowerCase();
|
|
1179
|
+
trackSkillsFromValue(tabId, nested, { keyHint: hint, kind, source, depth: depth + 1 });
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function trackSkillsFromToolInvocation(tabId, toolName, args, { sourcePrefix = "tool" } = {}) {
|
|
1184
|
+
if (!tabId) return;
|
|
1185
|
+
const name = String(toolName || "").trim();
|
|
1186
|
+
if (name.toLowerCase() !== "read") return;
|
|
1187
|
+
const source = `${sourcePrefix}:${name}`;
|
|
1188
|
+
trackSkillsFromValue(tabId, args, { kind: "read", source });
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
function trackSkillsFromMessage(tabId, message) {
|
|
1192
|
+
if (!tabId || !message) return;
|
|
1193
|
+
const role = String(message.role || "");
|
|
1194
|
+
if (role === "toolExecution" || role === "toolCall") {
|
|
1195
|
+
trackSkillsFromToolInvocation(tabId, message.toolName || message.name, message.arguments ?? message.args ?? {}, { sourcePrefix: `message:${role}` });
|
|
1196
|
+
return;
|
|
1197
|
+
}
|
|
1198
|
+
if (role === "user" || role === "assistant" || role === "assistantEvent" || role === "native") {
|
|
1199
|
+
trackSkillsFromText(tabId, textFromContent(message.content), { kind: "used", source: `message:${role}` });
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
if (role === "bashExecution") {
|
|
1203
|
+
trackSkillsFromText(tabId, `${message.command || ""}\n${message.output || ""}`, { kind: "used", source: "message:bash" });
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function trackSkillsFromMessages(messages = latestMessages, tabId = activeTabId) {
|
|
1208
|
+
for (const message of messages || []) trackSkillsFromMessage(tabId, message);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function trackSkillsFromEvent(event) {
|
|
1212
|
+
const tabId = event?.tabId || activeTabId;
|
|
1213
|
+
if (!tabId || !event) return;
|
|
1214
|
+
if (["tool_execution_start", "tool_execution_update", "tool_execution_end"].includes(event.type)) {
|
|
1215
|
+
trackSkillsFromToolInvocation(tabId, event.toolName, event.args, { sourcePrefix: `event:${event.type}` });
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
if (event.type === "message_update") {
|
|
1219
|
+
const update = event.assistantMessageEvent || {};
|
|
1220
|
+
if (update.type === "toolcall_start") {
|
|
1221
|
+
trackSkillsFromToolInvocation(tabId, update.name || update.toolName || update.toolCall?.name, update.arguments || update.args || update.toolCall?.arguments || {}, { sourcePrefix: "event:message_update" });
|
|
1222
|
+
}
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
if (event.type === "response" && event.command === "new_session") {
|
|
1226
|
+
clearSkillUsageForTab(tabId);
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function skillPathForEntry(entry) {
|
|
1231
|
+
if (entry?.path) return entry.path;
|
|
1232
|
+
if (entry?.paths instanceof Set && entry.paths.size) {
|
|
1233
|
+
const paths = [...entry.paths];
|
|
1234
|
+
return paths[paths.length - 1] || "";
|
|
1235
|
+
}
|
|
1236
|
+
return "";
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
function skillEditorApiPath({ name = "", path = "" } = {}) {
|
|
1240
|
+
const params = new URLSearchParams();
|
|
1241
|
+
if (name) params.set("name", name);
|
|
1242
|
+
if (path) params.set("path", path);
|
|
1243
|
+
const query = params.toString();
|
|
1244
|
+
return query ? `/api/skill-file?${query}` : "/api/skill-file";
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function setSkillEditorStatus(message = "", level = "muted") {
|
|
1248
|
+
const status = elements.skillEditorStatus;
|
|
1249
|
+
if (!status) return;
|
|
1250
|
+
status.textContent = message;
|
|
1251
|
+
status.className = `skill-editor-status ${level || "muted"}`;
|
|
1252
|
+
status.hidden = !message;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function closeSkillEditor() {
|
|
1256
|
+
if (elements.skillEditorDialog?.open) elements.skillEditorDialog.close();
|
|
1257
|
+
else activeSkillEditor = null;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function updateSkillEditorMeta(data = activeSkillEditor || {}) {
|
|
1261
|
+
if (!elements.skillEditorMeta) return;
|
|
1262
|
+
const parts = [data.name ? `Skill: ${data.name}` : "Skill", data.path || "path unavailable"].filter(Boolean);
|
|
1263
|
+
elements.skillEditorMeta.textContent = parts.join(" · ");
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
async function openSkillEditor(entry) {
|
|
1267
|
+
const name = normalizeSkillName(entry?.name || "");
|
|
1268
|
+
const path = skillPathForEntry(entry);
|
|
1269
|
+
if (!name || !elements.skillEditorDialog || !elements.skillEditorText) return;
|
|
1270
|
+
const tabId = activeTabId;
|
|
1271
|
+
activeSkillEditor = { name, path, tabId, mtimeMs: null };
|
|
1272
|
+
if (elements.skillEditorTitle) elements.skillEditorTitle.textContent = `Edit skill: ${name}`;
|
|
1273
|
+
if (elements.skillEditorText) elements.skillEditorText.value = "";
|
|
1274
|
+
if (elements.skillEditorSaveButton) elements.skillEditorSaveButton.disabled = true;
|
|
1275
|
+
updateSkillEditorMeta(activeSkillEditor);
|
|
1276
|
+
setSkillEditorStatus("Loading skill context…", "muted");
|
|
1277
|
+
if (!elements.skillEditorDialog.open) elements.skillEditorDialog.showModal();
|
|
1278
|
+
|
|
1279
|
+
try {
|
|
1280
|
+
const response = await api(skillEditorApiPath({ name, path }), { tabId });
|
|
1281
|
+
if (activeSkillEditor?.tabId !== tabId || activeSkillEditor?.name !== name) return;
|
|
1282
|
+
const data = response.data || {};
|
|
1283
|
+
activeSkillEditor = { name: normalizeSkillName(data.name || name), path: data.path || path, tabId, mtimeMs: data.mtimeMs };
|
|
1284
|
+
if (elements.skillEditorTitle) elements.skillEditorTitle.textContent = `Edit skill: ${activeSkillEditor.name}`;
|
|
1285
|
+
if (elements.skillEditorText) elements.skillEditorText.value = data.content || "";
|
|
1286
|
+
if (elements.skillEditorSaveButton) elements.skillEditorSaveButton.disabled = false;
|
|
1287
|
+
updateSkillEditorMeta(activeSkillEditor);
|
|
1288
|
+
setSkillEditorStatus("Edit this SKILL.md, then save. Reload the tab if title/description metadata should refresh immediately.", "muted");
|
|
1289
|
+
queueMicrotask(() => elements.skillEditorText?.focus());
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
if (elements.skillEditorSaveButton) elements.skillEditorSaveButton.disabled = true;
|
|
1292
|
+
setSkillEditorStatus(`Failed to open skill: ${error.message || String(error)}`, "error");
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
async function saveSkillEditor() {
|
|
1297
|
+
if (!activeSkillEditor || !elements.skillEditorText || !elements.skillEditorSaveButton) return;
|
|
1298
|
+
const editor = activeSkillEditor;
|
|
1299
|
+
const previousLabel = elements.skillEditorSaveButton.textContent;
|
|
1300
|
+
elements.skillEditorSaveButton.disabled = true;
|
|
1301
|
+
elements.skillEditorSaveButton.textContent = "Saving…";
|
|
1302
|
+
setSkillEditorStatus("Saving skill…", "muted");
|
|
1303
|
+
try {
|
|
1304
|
+
const response = await api("/api/skill-file", {
|
|
1305
|
+
method: "POST",
|
|
1306
|
+
tabId: editor.tabId,
|
|
1307
|
+
body: {
|
|
1308
|
+
name: editor.name,
|
|
1309
|
+
path: editor.path,
|
|
1310
|
+
mtimeMs: editor.mtimeMs,
|
|
1311
|
+
content: elements.skillEditorText.value,
|
|
1312
|
+
},
|
|
1313
|
+
});
|
|
1314
|
+
const data = response.data || {};
|
|
1315
|
+
const savedName = normalizeSkillName(data.name || editor.name);
|
|
1316
|
+
activeSkillEditor = { name: savedName, path: data.path || editor.path, tabId: editor.tabId, mtimeMs: data.mtimeMs };
|
|
1317
|
+
const map = skillUsageMapForTab(editor.tabId, { create: false });
|
|
1318
|
+
if (map && savedName !== editor.name) map.delete(editor.name);
|
|
1319
|
+
trackSkillUsage(editor.tabId, savedName, "read", "skill-editor", { path: activeSkillEditor.path });
|
|
1320
|
+
if (elements.skillEditorTitle) elements.skillEditorTitle.textContent = `Edit skill: ${savedName}`;
|
|
1321
|
+
updateSkillEditorMeta(activeSkillEditor);
|
|
1322
|
+
setSkillEditorStatus("Saved SKILL.md. Reload/restart affected tabs before relying on updated skill metadata or newly loaded instructions.", "ok");
|
|
1323
|
+
} catch (error) {
|
|
1324
|
+
setSkillEditorStatus(`Failed to save skill: ${error.message || String(error)}`, "error");
|
|
1325
|
+
} finally {
|
|
1326
|
+
elements.skillEditorSaveButton.textContent = previousLabel || "Save skill";
|
|
1327
|
+
elements.skillEditorSaveButton.disabled = false;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function renderBusyPromptBehaviorTag() {
|
|
1332
|
+
const tag = elements.busyPromptBehaviorTag;
|
|
1333
|
+
if (!tag) return;
|
|
1334
|
+
const behavior = normalizeBusyPromptBehavior(busyPromptBehavior);
|
|
1335
|
+
const label = BUSY_PROMPT_BEHAVIOR_LABELS[behavior] || BUSY_PROMPT_BEHAVIOR_LABELS.followUp;
|
|
1336
|
+
tag.textContent = label;
|
|
1337
|
+
tag.classList.toggle("follow-up", behavior === "followUp");
|
|
1338
|
+
tag.classList.toggle("steer", behavior === "steer");
|
|
1339
|
+
tag.title = behavior === "steer"
|
|
1340
|
+
? "While Pi is running, normal prompt submit steers the active run. Click to change."
|
|
1341
|
+
: "While Pi is running, normal prompt submit queues a follow-up. Click to change.";
|
|
1342
|
+
tag.setAttribute("aria-label", tag.title);
|
|
1343
|
+
renderBusyPromptBehaviorMenu();
|
|
1344
|
+
renderSessionSkillTags(activeTabId);
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function setBusyPromptBehaviorMenuOpen(open, { focusCurrent = false } = {}) {
|
|
1348
|
+
busyPromptBehaviorMenuOpen = !!open;
|
|
1349
|
+
elements.busyPromptBehaviorTag?.setAttribute("aria-expanded", busyPromptBehaviorMenuOpen ? "true" : "false");
|
|
1350
|
+
elements.busyPromptBehaviorTag?.classList.toggle("menu-open", busyPromptBehaviorMenuOpen);
|
|
1351
|
+
if (elements.busyPromptBehaviorMenu) elements.busyPromptBehaviorMenu.hidden = !busyPromptBehaviorMenuOpen;
|
|
1352
|
+
if (!busyPromptBehaviorMenuOpen) return;
|
|
1353
|
+
renderBusyPromptBehaviorMenu();
|
|
1354
|
+
if (focusCurrent) {
|
|
1355
|
+
requestAnimationFrame(() => {
|
|
1356
|
+
const current = busyPromptBehaviorMenuItems().find((item) => item.getAttribute("aria-checked") === "true") || busyPromptBehaviorMenuItems()[0];
|
|
1357
|
+
current?.focus({ preventScroll: true });
|
|
1358
|
+
});
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function focusBusyPromptBehaviorMenuItem(direction = 1) {
|
|
1363
|
+
const items = busyPromptBehaviorMenuItems();
|
|
1364
|
+
if (!items.length) return;
|
|
1365
|
+
const currentIndex = Math.max(0, items.indexOf(document.activeElement));
|
|
1366
|
+
const nextIndex = (currentIndex + direction + items.length) % items.length;
|
|
1367
|
+
items[nextIndex].focus({ preventScroll: true });
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function chooseBusyPromptBehaviorFromMenu(value) {
|
|
1371
|
+
setBusyPromptBehavior(value);
|
|
1372
|
+
setBusyPromptBehaviorMenuOpen(false);
|
|
1373
|
+
focusPromptInput({ defer: true });
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function setBusyPromptBehavior(value, { persist = true } = {}) {
|
|
1377
|
+
const next = normalizeBusyPromptBehavior(value);
|
|
1378
|
+
busyPromptBehavior = next;
|
|
1379
|
+
webuiSettings = { ...webuiSettings, busyPromptBehavior: next };
|
|
1380
|
+
if (persist) persistBusyPromptBehavior(next);
|
|
1381
|
+
renderBusyPromptBehaviorTag();
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function restoreBusyPromptBehaviorSetting() {
|
|
1385
|
+
setBusyPromptBehavior(readStoredBusyPromptBehavior(), { persist: false });
|
|
1386
|
+
}
|
|
1387
|
+
|
|
908
1388
|
function clampAutocompleteMaxVisible(value) {
|
|
909
1389
|
const number = Number(value);
|
|
910
1390
|
if (!Number.isFinite(number)) return 12;
|
|
@@ -917,6 +1397,7 @@ function applyNativeSettingsForBrowser(settings = {}, { syncThinkingVisibility =
|
|
|
917
1397
|
if (settings.autocompleteMaxVisible !== undefined) autocompleteMaxVisible = clampAutocompleteMaxVisible(settings.autocompleteMaxVisible);
|
|
918
1398
|
if (SETTINGS_DOUBLE_ESCAPE_OPTIONS.some((option) => option.value === settings.doubleEscapeAction)) doubleEscapeAction = settings.doubleEscapeAction;
|
|
919
1399
|
if (SETTINGS_TREE_FILTER_OPTIONS.includes(settings.treeFilterMode)) treeFilterMode = settings.treeFilterMode;
|
|
1400
|
+
if (BUSY_PROMPT_BEHAVIOR_VALUES.has(settings.busyPromptBehavior)) setBusyPromptBehavior(settings.busyPromptBehavior);
|
|
920
1401
|
if (syncThinkingVisibility && typeof settings.hideThinkingBlock === "boolean") setThinkingOutputVisible(!settings.hideThinkingBlock);
|
|
921
1402
|
}
|
|
922
1403
|
|
|
@@ -936,6 +1417,7 @@ function setComposerActionsOpen(open) {
|
|
|
936
1417
|
setNativeCommandMenuOpen(false);
|
|
937
1418
|
setAppRunnerMenuOpen(false);
|
|
938
1419
|
setOptionsMenuOpen(false);
|
|
1420
|
+
setBusyPromptBehaviorMenuOpen(false);
|
|
939
1421
|
}
|
|
940
1422
|
}
|
|
941
1423
|
|
|
@@ -993,6 +1475,7 @@ function updateComposerModeButtons() {
|
|
|
993
1475
|
elements.abortButton.textContent = abortRequestInFlight ? "Aborting…" : "Abort";
|
|
994
1476
|
elements.abortButton.title = abortAvailable ? "Abort the active Pi run (Esc or hold)" : "Abort is available while Pi is running";
|
|
995
1477
|
elements.abortButton.setAttribute("aria-label", elements.abortButton.title);
|
|
1478
|
+
renderBusyPromptBehaviorTag();
|
|
996
1479
|
document.body.classList.toggle("pi-run-active", runActive || abortAvailable);
|
|
997
1480
|
}
|
|
998
1481
|
|
|
@@ -1228,6 +1711,7 @@ function setServerRestartOverlay(active, message = "Waiting for the server to co
|
|
|
1228
1711
|
document.body.classList.toggle("server-restarting", serverRestartInProgress);
|
|
1229
1712
|
if (elements.serverRestartPanel) elements.serverRestartPanel.hidden = !serverRestartInProgress;
|
|
1230
1713
|
if (elements.serverRestartMessage) elements.serverRestartMessage.textContent = message;
|
|
1714
|
+
if (serverRestartInProgress) hideUpdateNotification();
|
|
1231
1715
|
if (serverRestartInProgress && elements.serverOfflinePanel) elements.serverOfflinePanel.hidden = true;
|
|
1232
1716
|
}
|
|
1233
1717
|
|
|
@@ -1238,6 +1722,7 @@ function setBackendOffline(offline, error) {
|
|
|
1238
1722
|
if (elements.serverOfflinePanel) elements.serverOfflinePanel.hidden = !showOfflinePanel;
|
|
1239
1723
|
renderServerOfflinePanel();
|
|
1240
1724
|
if (backendOffline) {
|
|
1725
|
+
hideUpdateNotification();
|
|
1241
1726
|
if (!serverRestartInProgress && !backendOfflineNoticeShown) {
|
|
1242
1727
|
backendOfflineNoticeShown = true;
|
|
1243
1728
|
addEvent(`Pi Web UI server is offline${error?.message ? `: ${error.message}` : ""}`, "warn");
|
|
@@ -1480,6 +1965,169 @@ async function refreshWebuiVersion() {
|
|
|
1480
1965
|
setWebuiDevServer(isWebuiDevMetadata(health));
|
|
1481
1966
|
}
|
|
1482
1967
|
|
|
1968
|
+
function packageUpdateText(label, status = {}) {
|
|
1969
|
+
const current = formatWebuiVersion(status.currentVersion || "");
|
|
1970
|
+
const latest = formatWebuiVersion(status.latestVersion || "");
|
|
1971
|
+
if (current && latest) return `${label} ${current} → ${latest}`;
|
|
1972
|
+
if (latest) return `${label} ${latest}`;
|
|
1973
|
+
return label;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
function updateNotificationItems(status = latestUpdateStatus) {
|
|
1977
|
+
const items = [];
|
|
1978
|
+
if (status?.pi?.updateAvailable) items.push(packageUpdateText("Pi", status.pi));
|
|
1979
|
+
if (status?.webui?.updateAvailable) items.push(packageUpdateText("Web UI", status.webui));
|
|
1980
|
+
return items;
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
function updateNotificationDismissKey(status = latestUpdateStatus) {
|
|
1984
|
+
const parts = [status?.pi?.latestVersion, status?.webui?.latestVersion]
|
|
1985
|
+
.map((value) => String(value || "").trim())
|
|
1986
|
+
.filter(Boolean);
|
|
1987
|
+
return parts.length ? parts.join("|") : "";
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
function storedDismissedUpdateKey() {
|
|
1991
|
+
try {
|
|
1992
|
+
return localStorage.getItem(UPDATE_NOTIFICATION_DISMISS_STORAGE_KEY) || "";
|
|
1993
|
+
} catch {
|
|
1994
|
+
return "";
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
function rememberDismissedUpdateKey(key) {
|
|
1999
|
+
if (!key) return;
|
|
2000
|
+
try {
|
|
2001
|
+
localStorage.setItem(UPDATE_NOTIFICATION_DISMISS_STORAGE_KEY, key);
|
|
2002
|
+
} catch {
|
|
2003
|
+
// Ignore private-mode storage failures.
|
|
2004
|
+
}
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
function hideUpdateNotification({ remember = false } = {}) {
|
|
2008
|
+
const panel = elements.updateNotification;
|
|
2009
|
+
if (!panel) return;
|
|
2010
|
+
clearTimeout(updateNotificationHideTimer);
|
|
2011
|
+
if (remember) rememberDismissedUpdateKey(updateNotificationDismissKey());
|
|
2012
|
+
panel.classList.remove("show");
|
|
2013
|
+
updateNotificationHideTimer = setTimeout(() => {
|
|
2014
|
+
panel.hidden = true;
|
|
2015
|
+
}, 360);
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
function renderUpdateNotification(status = latestUpdateStatus, { force = false } = {}) {
|
|
2019
|
+
const panel = elements.updateNotification;
|
|
2020
|
+
if (!panel) return;
|
|
2021
|
+
latestUpdateStatus = status || latestUpdateStatus;
|
|
2022
|
+
const items = updateNotificationItems(latestUpdateStatus);
|
|
2023
|
+
const dismissKey = updateNotificationDismissKey(latestUpdateStatus);
|
|
2024
|
+
const shouldShow = !!latestUpdateStatus?.updateAvailable && items.length > 0 && !updateRequestInProgress;
|
|
2025
|
+
if (!shouldShow || (!force && dismissKey && storedDismissedUpdateKey() === dismissKey)) {
|
|
2026
|
+
hideUpdateNotification();
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
const canRunUpdate = latestUpdateStatus.canRunUpdate !== false;
|
|
2031
|
+
if (elements.updateNotificationTitle) elements.updateNotificationTitle.textContent = items.length === 1 ? `${items[0]} available` : "Pi updates available";
|
|
2032
|
+
if (elements.updateNotificationMessage) {
|
|
2033
|
+
elements.updateNotificationMessage.textContent = canRunUpdate
|
|
2034
|
+
? "Run pi update now, then restart this Web UI server automatically."
|
|
2035
|
+
: "Updates are available. Direct Web UI updates are only enabled from localhost on the host machine.";
|
|
2036
|
+
}
|
|
2037
|
+
const details = [
|
|
2038
|
+
items.join(" · "),
|
|
2039
|
+
latestUpdateStatus.webuiDev && latestUpdateStatus.webui?.updateAvailable ? "The current Web UI is a dev checkout; pi update updates installed Pi packages, not this checkout." : "",
|
|
2040
|
+
latestUpdateStatus.packages?.note || "",
|
|
2041
|
+
].filter(Boolean).join(" ");
|
|
2042
|
+
if (elements.updateNotificationDetail) elements.updateNotificationDetail.textContent = details;
|
|
2043
|
+
if (elements.updateNotificationUpdateButton) {
|
|
2044
|
+
elements.updateNotificationUpdateButton.hidden = !canRunUpdate;
|
|
2045
|
+
elements.updateNotificationUpdateButton.disabled = updateRequestInProgress || latestUpdateStatus.updateInProgress;
|
|
2046
|
+
elements.updateNotificationUpdateButton.textContent = latestUpdateStatus.updateInProgress ? "Updating…" : "Update & restart";
|
|
2047
|
+
}
|
|
2048
|
+
clearTimeout(updateNotificationHideTimer);
|
|
2049
|
+
panel.hidden = false;
|
|
2050
|
+
requestAnimationFrame(() => panel.classList.add("show"));
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
async function refreshUpdateStatus({ force = false, notify = true } = {}) {
|
|
2054
|
+
const path = force ? "/api/update-status?refresh=1" : "/api/update-status";
|
|
2055
|
+
const response = await api(path, { scoped: false });
|
|
2056
|
+
latestUpdateStatus = response.data || null;
|
|
2057
|
+
if (notify) renderUpdateNotification(latestUpdateStatus);
|
|
2058
|
+
return latestUpdateStatus;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
function scheduleUpdateStatusRefresh() {
|
|
2062
|
+
clearTimeout(updateStatusRefreshTimer);
|
|
2063
|
+
updateStatusRefreshTimer = setTimeout(() => {
|
|
2064
|
+
updateStatusRefreshTimer = null;
|
|
2065
|
+
refreshUpdateStatus({ force: true }).catch((error) => addEvent(`Pi update check failed: ${error.message || String(error)}`, "warn"));
|
|
2066
|
+
scheduleUpdateStatusRefresh();
|
|
2067
|
+
}, UPDATE_STATUS_REFRESH_MS);
|
|
2068
|
+
}
|
|
2069
|
+
|
|
2070
|
+
function initializeUpdateNotifications() {
|
|
2071
|
+
setTimeout(() => {
|
|
2072
|
+
refreshUpdateStatus().catch((error) => addEvent(`Pi update check failed: ${error.message || String(error)}`, "warn"));
|
|
2073
|
+
scheduleUpdateStatusRefresh();
|
|
2074
|
+
}, UPDATE_STATUS_INITIAL_DELAY_MS);
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
function piUpdateConfirmationText() {
|
|
2078
|
+
const items = updateNotificationItems();
|
|
2079
|
+
const workingWarning = hasWorkingTab() ? "\n\nOne or more Pi tabs look busy or blocked. Finish or abort in-flight work before updating if you need to preserve it." : "";
|
|
2080
|
+
const versionText = items.length ? `\n\nDetected update: ${items.join(" · ")}.` : "";
|
|
2081
|
+
return `Run pi update now?${versionText}\n\nThis will run \"pi update\" on the Web UI host. After it finishes, Pi Web UI will restart itself. Browser clients will briefly disconnect, and managed Pi tabs/RPC processes will be restarted from saved session state when possible.${workingWarning}`;
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
async function runPiUpdateAndRestart() {
|
|
2085
|
+
if (updateRequestInProgress) return;
|
|
2086
|
+
if (latestUpdateStatus?.canRunUpdate === false) {
|
|
2087
|
+
addEvent("Pi update can only be started from localhost on the Web UI host", "warn");
|
|
2088
|
+
renderUpdateNotification(latestUpdateStatus, { force: true });
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
if (!confirm(piUpdateConfirmationText())) return;
|
|
2092
|
+
|
|
2093
|
+
updateRequestInProgress = true;
|
|
2094
|
+
hideUpdateNotification();
|
|
2095
|
+
setServerActionBusy("Updating…");
|
|
2096
|
+
setServerActionStatus("Running pi update. The server will restart after the update completes…", "warn");
|
|
2097
|
+
setServerRestartOverlay(true, "Running pi update. The server will restart after the update completes…");
|
|
2098
|
+
try {
|
|
2099
|
+
await api("/api/update", { method: "POST", scoped: false });
|
|
2100
|
+
addEvent("Pi update completed; Pi Web UI server restart requested", "warn");
|
|
2101
|
+
} catch (error) {
|
|
2102
|
+
if (!error?.backendOffline) {
|
|
2103
|
+
updateRequestInProgress = false;
|
|
2104
|
+
setServerRestartOverlay(false);
|
|
2105
|
+
resetServerActionControls();
|
|
2106
|
+
const message = error.message || String(error);
|
|
2107
|
+
setServerActionStatus(message, "error");
|
|
2108
|
+
addEvent(message, "error");
|
|
2109
|
+
renderUpdateNotification(latestUpdateStatus, { force: true });
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
addEvent("Pi Web UI server connection dropped during update restart request", "warn");
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
setBackendOffline(true, new Error("update requested from side panel"));
|
|
2116
|
+
const restarted = await waitForServerRestart();
|
|
2117
|
+
updateRequestInProgress = false;
|
|
2118
|
+
resetServerActionControls();
|
|
2119
|
+
if (restarted) {
|
|
2120
|
+
hideUpdateNotification({ remember: true });
|
|
2121
|
+
setServerActionStatus("Updated, restarted, and reconnected.", "success");
|
|
2122
|
+
refreshUpdateStatus({ force: true, notify: false }).catch(() => {});
|
|
2123
|
+
} else {
|
|
2124
|
+
setServerRestartOverlay(false);
|
|
2125
|
+
setBackendOffline(true, new Error("update restart reconnect timed out"));
|
|
2126
|
+
setServerActionStatus("Update completed, but the server did not reconnect automatically.", "error");
|
|
2127
|
+
addEvent("Pi Web UI server did not come back online after update request", "error");
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
|
|
1483
2131
|
function formatBytes(bytes) {
|
|
1484
2132
|
const value = Number(bytes) || 0;
|
|
1485
2133
|
if (value < 1024) return `${value} B`;
|
|
@@ -2717,9 +3365,11 @@ function syncTabMetadata(nextTabs = []) {
|
|
|
2717
3365
|
tabActivities.delete(tabId);
|
|
2718
3366
|
tabSeenCompletionSerials.delete(tabId);
|
|
2719
3367
|
actionFeedbackByTab.delete(tabId);
|
|
3368
|
+
skillUsageByTab.delete(tabId);
|
|
2720
3369
|
clearGitWorkflowForTab(tabId);
|
|
2721
3370
|
}
|
|
2722
3371
|
}
|
|
3372
|
+
pruneSkillUsageForKnownTabs(liveIds);
|
|
2723
3373
|
}
|
|
2724
3374
|
|
|
2725
3375
|
function applyTabMetadata(tab) {
|
|
@@ -2926,7 +3576,7 @@ function restoreActiveDraft() {
|
|
|
2926
3576
|
|
|
2927
3577
|
function focusPromptInput({ defer = false } = {}) {
|
|
2928
3578
|
const focus = () => {
|
|
2929
|
-
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;
|
|
3579
|
+
if (!elements.promptInput || elements.dialog.open || elements.pathPickerDialog.open || elements.nativeCommandDialog.open || elements.appRunnerInfoDialog?.open || elements.promptListDialog?.open || elements.attachmentTextDialog?.open || elements.skillEditorDialog?.open || document.visibilityState === "hidden") return;
|
|
2930
3580
|
try {
|
|
2931
3581
|
elements.promptInput.focus({ preventScroll: true });
|
|
2932
3582
|
} catch {
|
|
@@ -3281,6 +3931,7 @@ async function refreshTabs({ selectStored = false } = {}) {
|
|
|
3281
3931
|
setActiveTabId((requested && tabs.some((tab) => tab.id === requested) ? requested : stored && tabs.some((tab) => tab.id === stored) ? stored : tabs[0]?.id) || null, { remember: true });
|
|
3282
3932
|
}
|
|
3283
3933
|
rememberServerStartCwd(tabs.find((tab) => tab.id === activeTabId)?.cwd || tabs[0]?.cwd);
|
|
3934
|
+
renderSessionSkillTags(activeTabId);
|
|
3284
3935
|
renderTabs();
|
|
3285
3936
|
return tabs;
|
|
3286
3937
|
}
|
|
@@ -10205,7 +10856,7 @@ async function openNativeSettingsDialog() {
|
|
|
10205
10856
|
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 } }));
|
|
10206
10857
|
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 } }));
|
|
10207
10858
|
if (controls.autoCompact.input.checked !== (state.autoCompactionEnabled !== false)) requests.push(nativeCommandApi("/api/auto-compaction", { method: "POST", body: { enabled: controls.autoCompact.input.checked } }));
|
|
10208
|
-
|
|
10859
|
+
setBusyPromptBehavior(controls.busyBehavior.select.value);
|
|
10209
10860
|
if (controls.thinkingOutput.input.checked !== thinkingOutputVisible) setThinkingOutputVisible(controls.thinkingOutput.input.checked);
|
|
10210
10861
|
if (controls.doneNotifications.input.checked !== agentDoneNotificationsEnabled) await setAgentDoneNotificationsEnabled(controls.doneNotifications.input.checked);
|
|
10211
10862
|
await Promise.all(requests);
|
|
@@ -10678,6 +11329,7 @@ function renderMessages(messages) {
|
|
|
10678
11329
|
cleanupLiveToolRunsForMessages(latestMessages);
|
|
10679
11330
|
syncLastUserPromptFromMessages(latestMessages);
|
|
10680
11331
|
syncPromptHistoryFromMessages(latestMessages);
|
|
11332
|
+
trackSkillsFromMessages(latestMessages, activeTabId);
|
|
10681
11333
|
renderAllMessages();
|
|
10682
11334
|
renderFooter();
|
|
10683
11335
|
renderFeedbackTray();
|
|
@@ -11610,9 +12262,11 @@ function updateServerActionButton() {
|
|
|
11610
12262
|
const button = elements.runServerActionButton;
|
|
11611
12263
|
if (!button) return;
|
|
11612
12264
|
button.disabled = !action;
|
|
11613
|
-
button.textContent = action === "restart" ? "Restart" : action === "stop" ? "Stop" : "Run";
|
|
12265
|
+
button.textContent = action === "restart" ? "Restart" : action === "update" ? "Update" : action === "stop" ? "Stop" : "Run";
|
|
11614
12266
|
button.classList.toggle("danger", action === "stop");
|
|
11615
|
-
if (action
|
|
12267
|
+
if (action === "restart") setServerActionStatus("Ready to restart the Web UI server.", "info");
|
|
12268
|
+
else if (action === "update") setServerActionStatus("Ready to run pi update, then restart the Web UI server.", "info");
|
|
12269
|
+
else if (action === "stop") setServerActionStatus("Ready to stop the Web UI server.", "info");
|
|
11616
12270
|
else setServerActionStatus();
|
|
11617
12271
|
}
|
|
11618
12272
|
|
|
@@ -11718,6 +12372,7 @@ async function stopServer() {
|
|
|
11718
12372
|
async function runSelectedServerAction() {
|
|
11719
12373
|
const action = elements.serverActionSelect?.value || "";
|
|
11720
12374
|
if (action === "restart") await restartServer();
|
|
12375
|
+
else if (action === "update") await runPiUpdateAndRestart();
|
|
11721
12376
|
else if (action === "stop") await stopServer();
|
|
11722
12377
|
}
|
|
11723
12378
|
|
|
@@ -11927,7 +12582,7 @@ async function sendPrompt(kind = "prompt", explicitMessage, { targetTabId = acti
|
|
|
11927
12582
|
}
|
|
11928
12583
|
|
|
11929
12584
|
const targetWasStreaming = !!currentState?.isStreaming;
|
|
11930
|
-
const busyBehavior = busyPromptBehavior
|
|
12585
|
+
const busyBehavior = normalizeBusyPromptBehavior(busyPromptBehavior);
|
|
11931
12586
|
const startsRun = kind === "prompt" && !targetWasStreaming;
|
|
11932
12587
|
autoFollowChat = true;
|
|
11933
12588
|
updateJumpToLatestButton();
|
|
@@ -12188,6 +12843,7 @@ function handleInactiveTabEvent(event) {
|
|
|
12188
12843
|
|
|
12189
12844
|
function handleEvent(event) {
|
|
12190
12845
|
ingestEventTabActivity(event);
|
|
12846
|
+
trackSkillsFromEvent(event);
|
|
12191
12847
|
if (!eventTargetsActiveTab(event)) {
|
|
12192
12848
|
handleInactiveTabEvent(event);
|
|
12193
12849
|
return;
|
|
@@ -12485,6 +13141,20 @@ elements.attachmentTextDialog?.addEventListener("keydown", (event) => {
|
|
|
12485
13141
|
if (!elements.attachmentTextSaveButton?.disabled) saveTextAttachmentEdit();
|
|
12486
13142
|
});
|
|
12487
13143
|
elements.attachmentTextDialog?.querySelector("form")?.addEventListener("submit", (event) => event.preventDefault());
|
|
13144
|
+
elements.skillEditorCancelButton?.addEventListener("click", closeSkillEditor);
|
|
13145
|
+
elements.skillEditorSaveButton?.addEventListener("click", saveSkillEditor);
|
|
13146
|
+
elements.skillEditorText?.addEventListener("input", () => setSkillEditorStatus("Unsaved skill edits.", "warn"));
|
|
13147
|
+
elements.skillEditorDialog?.addEventListener("close", () => {
|
|
13148
|
+
activeSkillEditor = null;
|
|
13149
|
+
if (elements.skillEditorText) elements.skillEditorText.value = "";
|
|
13150
|
+
setSkillEditorStatus("");
|
|
13151
|
+
});
|
|
13152
|
+
elements.skillEditorDialog?.addEventListener("keydown", (event) => {
|
|
13153
|
+
if (!(event.ctrlKey || event.metaKey) || event.altKey || event.shiftKey || event.key.toLowerCase() !== "s") return;
|
|
13154
|
+
event.preventDefault();
|
|
13155
|
+
if (!elements.skillEditorSaveButton?.disabled) saveSkillEditor();
|
|
13156
|
+
});
|
|
13157
|
+
elements.skillEditorDialog?.querySelector("form")?.addEventListener("submit", (event) => event.preventDefault());
|
|
12488
13158
|
elements.sendFeedbackButton.addEventListener("click", () => submitQueuedActionFeedback());
|
|
12489
13159
|
elements.composer.addEventListener("submit", (event) => {
|
|
12490
13160
|
event.preventDefault();
|
|
@@ -12493,6 +13163,56 @@ elements.composer.addEventListener("submit", (event) => {
|
|
|
12493
13163
|
elements.composerActionsButton.addEventListener("click", () => {
|
|
12494
13164
|
setComposerActionsOpen(!document.body.classList.contains("composer-actions-open"));
|
|
12495
13165
|
});
|
|
13166
|
+
elements.busyPromptBehaviorTag?.addEventListener("click", (event) => {
|
|
13167
|
+
event.preventDefault();
|
|
13168
|
+
const nextOpen = !busyPromptBehaviorMenuOpen;
|
|
13169
|
+
setPublishMenuOpen(false);
|
|
13170
|
+
setNativeCommandMenuOpen(false);
|
|
13171
|
+
setAppRunnerMenuOpen(false);
|
|
13172
|
+
setOptionsMenuOpen(false);
|
|
13173
|
+
setComposerActionsOpen(false);
|
|
13174
|
+
setBusyPromptBehaviorMenuOpen(nextOpen);
|
|
13175
|
+
});
|
|
13176
|
+
elements.busyPromptBehaviorTag?.addEventListener("keydown", (event) => {
|
|
13177
|
+
if (!["ArrowDown", "ArrowUp", "Enter", " "].includes(event.key)) return;
|
|
13178
|
+
event.preventDefault();
|
|
13179
|
+
const focusPrevious = event.key === "ArrowUp";
|
|
13180
|
+
setBusyPromptBehaviorMenuOpen(true);
|
|
13181
|
+
requestAnimationFrame(() => {
|
|
13182
|
+
const items = busyPromptBehaviorMenuItems();
|
|
13183
|
+
if (!items.length) return;
|
|
13184
|
+
const currentIndex = Math.max(0, items.findIndex((item) => item.getAttribute("aria-checked") === "true"));
|
|
13185
|
+
const targetIndex = focusPrevious ? (currentIndex - 1 + items.length) % items.length : currentIndex;
|
|
13186
|
+
items[targetIndex]?.focus({ preventScroll: true });
|
|
13187
|
+
});
|
|
13188
|
+
});
|
|
13189
|
+
elements.busyPromptBehaviorMenu?.addEventListener("click", (event) => {
|
|
13190
|
+
const item = event.target?.closest?.("[data-busy-prompt-behavior]");
|
|
13191
|
+
if (!item) return;
|
|
13192
|
+
chooseBusyPromptBehaviorFromMenu(item.dataset.busyPromptBehavior);
|
|
13193
|
+
});
|
|
13194
|
+
elements.busyPromptBehaviorMenu?.addEventListener("keydown", (event) => {
|
|
13195
|
+
if (event.key === "Escape") {
|
|
13196
|
+
event.preventDefault();
|
|
13197
|
+
setBusyPromptBehaviorMenuOpen(false);
|
|
13198
|
+
elements.busyPromptBehaviorTag?.focus({ preventScroll: true });
|
|
13199
|
+
return;
|
|
13200
|
+
}
|
|
13201
|
+
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
|
13202
|
+
event.preventDefault();
|
|
13203
|
+
focusBusyPromptBehaviorMenuItem(event.key === "ArrowDown" ? 1 : -1);
|
|
13204
|
+
return;
|
|
13205
|
+
}
|
|
13206
|
+
if (event.key === "Home" || event.key === "End") {
|
|
13207
|
+
event.preventDefault();
|
|
13208
|
+
const items = busyPromptBehaviorMenuItems();
|
|
13209
|
+
items[event.key === "Home" ? 0 : items.length - 1]?.focus({ preventScroll: true });
|
|
13210
|
+
return;
|
|
13211
|
+
}
|
|
13212
|
+
if (event.key === "Tab") {
|
|
13213
|
+
setBusyPromptBehaviorMenuOpen(false);
|
|
13214
|
+
}
|
|
13215
|
+
});
|
|
12496
13216
|
elements.steerButton.addEventListener("click", () => sendPromptFromModeButton("steer", elements.steerButton));
|
|
12497
13217
|
elements.followUpButton.addEventListener("click", () => sendPromptFromModeButton("follow-up", elements.followUpButton));
|
|
12498
13218
|
elements.terminalTabsToggleButton.addEventListener("click", () => {
|
|
@@ -12866,6 +13586,8 @@ if (elements.backgroundClearButton) {
|
|
|
12866
13586
|
elements.openNetworkButton.addEventListener("click", openToNetwork);
|
|
12867
13587
|
elements.serverActionSelect.addEventListener("change", updateServerActionButton);
|
|
12868
13588
|
elements.runServerActionButton.addEventListener("click", () => runSelectedServerAction().catch((error) => addEvent(error.message || String(error), "error")));
|
|
13589
|
+
elements.updateNotificationUpdateButton?.addEventListener("click", () => runPiUpdateAndRestart().catch((error) => addEvent(error.message || String(error), "error")));
|
|
13590
|
+
elements.updateNotificationDismissButton?.addEventListener("click", () => hideUpdateNotification({ remember: true }));
|
|
12869
13591
|
updateServerActionButton();
|
|
12870
13592
|
elements.agentDoneNotificationsToggle.addEventListener("change", () => {
|
|
12871
13593
|
setAgentDoneNotificationsEnabled(elements.agentDoneNotificationsToggle.checked, {
|
|
@@ -12929,6 +13651,9 @@ document.addEventListener("pointerdown", (event) => {
|
|
|
12929
13651
|
if (optionsMenuOpen && !event.target?.closest?.(".composer-options-menu")) {
|
|
12930
13652
|
setOptionsMenuOpen(false);
|
|
12931
13653
|
}
|
|
13654
|
+
if (busyPromptBehaviorMenuOpen && !event.target?.closest?.(".composer-context-tags, .composer-busy-mode-menu")) {
|
|
13655
|
+
setBusyPromptBehaviorMenuOpen(false);
|
|
13656
|
+
}
|
|
12932
13657
|
if (document.body.classList.contains("mobile-tabs-expanded") && !elements.tabBar.contains(event.target) && !elements.terminalTabsToggleButton.contains(event.target)) {
|
|
12933
13658
|
setNewTabMenuOpen(false);
|
|
12934
13659
|
setMobileTabsExpanded(false);
|
|
@@ -13029,6 +13754,11 @@ window.addEventListener("keydown", (event) => {
|
|
|
13029
13754
|
setOptionsMenuOpen(false);
|
|
13030
13755
|
return;
|
|
13031
13756
|
}
|
|
13757
|
+
if (busyPromptBehaviorMenuOpen) {
|
|
13758
|
+
setBusyPromptBehaviorMenuOpen(false);
|
|
13759
|
+
elements.busyPromptBehaviorTag?.focus({ preventScroll: true });
|
|
13760
|
+
return;
|
|
13761
|
+
}
|
|
13032
13762
|
if (newTabMenuOpen) {
|
|
13033
13763
|
setNewTabMenuOpen(false);
|
|
13034
13764
|
return;
|
|
@@ -13181,6 +13911,8 @@ elements.promptInput.addEventListener("blur", () => {
|
|
|
13181
13911
|
|
|
13182
13912
|
resizePromptInput();
|
|
13183
13913
|
focusPromptInput({ defer: true });
|
|
13914
|
+
restoreStoredSkillUsage();
|
|
13915
|
+
restoreBusyPromptBehaviorSetting();
|
|
13184
13916
|
updateComposerModeButtons();
|
|
13185
13917
|
updateOptionalFeatureAvailability();
|
|
13186
13918
|
renderAppRunnerControls();
|
|
@@ -13203,6 +13935,7 @@ restoreSidePanelSectionState();
|
|
|
13203
13935
|
bindSidePanelSectionToggles();
|
|
13204
13936
|
restoreSidePanelState();
|
|
13205
13937
|
initializeCodexUsage();
|
|
13938
|
+
initializeUpdateNotifications();
|
|
13206
13939
|
bindMobileViewChanges();
|
|
13207
13940
|
bindSidePanelOverlayViewChanges();
|
|
13208
13941
|
registerPwaServiceWorker();
|