@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/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
- busyPromptBehavior = controls.busyBehavior.select.value;
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) setServerActionStatus(action === "restart" ? "Ready to restart the Web UI server." : "Ready to stop the Web UI server.", "info");
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 || "followUp";
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();