@firstpick/pi-package-webui 0.2.4 → 0.2.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 +1 -1
- package/bin/pi-webui.mjs +46 -26
- package/package.json +1 -1
- package/public/app.js +521 -140
- package/public/index.html +23 -2
- package/public/service-worker.js +1 -1
- package/public/styles.css +179 -3
- package/tests/mobile-static.test.mjs +67 -14
package/public/app.js
CHANGED
|
@@ -43,6 +43,10 @@ const elements = {
|
|
|
43
43
|
publishMenu: $("#publishMenu"),
|
|
44
44
|
releaseNpmButton: $("#releaseNpmButton"),
|
|
45
45
|
releaseAurButton: $("#releaseAurButton"),
|
|
46
|
+
nativeCommandMenuButton: $("#nativeCommandMenuButton"),
|
|
47
|
+
nativeCommandMenu: $("#nativeCommandMenu"),
|
|
48
|
+
nativeSkillsButton: $("#nativeSkillsButton"),
|
|
49
|
+
nativeToolsButton: $("#nativeToolsButton"),
|
|
46
50
|
gitWorkflowPanel: $("#gitWorkflowPanel"),
|
|
47
51
|
gitWorkflowTitle: $("#gitWorkflowTitle"),
|
|
48
52
|
gitWorkflowHint: $("#gitWorkflowHint"),
|
|
@@ -143,6 +147,7 @@ let pathFastPicksReady = false;
|
|
|
143
147
|
let pathFastPicksLoadPromise = null;
|
|
144
148
|
let mobileTabsExpanded = false;
|
|
145
149
|
let openTerminalTabGroupKey = null;
|
|
150
|
+
let nativeCommandMenuOpen = false;
|
|
146
151
|
let availableCommands = [];
|
|
147
152
|
let rawAvailableCommands = [];
|
|
148
153
|
let commandSuggestions = [];
|
|
@@ -202,9 +207,6 @@ let mobileFooterExpanded = false;
|
|
|
202
207
|
let footerModelPickerOpen = false;
|
|
203
208
|
let publishMenuOpen = false;
|
|
204
209
|
let maxVisualViewportHeight = 0;
|
|
205
|
-
let currentRunStartedAt = null;
|
|
206
|
-
let currentRunStreamChars = 0;
|
|
207
|
-
let latestTokPerSecond = null;
|
|
208
210
|
let abortRequestInFlight = false;
|
|
209
211
|
let userBashByTab = new Map();
|
|
210
212
|
let userBashQueuesByTab = new Map();
|
|
@@ -229,6 +231,10 @@ const SERVER_START_CWD_STORAGE_KEY = "pi-webui-last-server-cwd";
|
|
|
229
231
|
const DEFAULT_WEBUI_PORT = "31415";
|
|
230
232
|
const CUSTOM_BACKGROUND_MAX_FILE_BYTES = 24 * 1024 * 1024;
|
|
231
233
|
const OPTIONAL_FEATURES_STORAGE_KEY = "pi-webui-optional-features-disabled";
|
|
234
|
+
const GIT_FOOTER_WEBUI_STATUS_KEY = "git-footer-webui";
|
|
235
|
+
const GIT_FOOTER_WEBUI_PAYLOAD_TYPE = "firstpick.git-footer-status.footer";
|
|
236
|
+
const GIT_FOOTER_WEBUI_PAYLOAD_VERSION = 1;
|
|
237
|
+
const GIT_FOOTER_WEBUI_PAYLOAD_CACHE_KEY = "pi-webui-git-footer-webui-payload-cache";
|
|
232
238
|
const LAST_USER_PROMPT_STORAGE_KEY = "pi-webui-last-user-prompts";
|
|
233
239
|
const PROMPT_HISTORY_STORAGE_KEY = "pi-webui-prompt-history";
|
|
234
240
|
const PROMPT_HISTORY_LIMIT_PER_TAB = 50;
|
|
@@ -338,8 +344,8 @@ const OPTIONAL_FEATURES = [
|
|
|
338
344
|
id: "gitFooterStatus",
|
|
339
345
|
label: "Git footer status",
|
|
340
346
|
packageName: "@firstpick/pi-extension-git-footer-status",
|
|
341
|
-
capabilityLabel: "/git-footer-refresh or git-footer status event",
|
|
342
|
-
description: "
|
|
347
|
+
capabilityLabel: "/git-footer-refresh or git-footer-webui status event",
|
|
348
|
+
description: "Extension-owned enhanced footer/status telemetry when loaded by Pi.",
|
|
343
349
|
},
|
|
344
350
|
{
|
|
345
351
|
id: "statsCommand",
|
|
@@ -361,6 +367,8 @@ const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
|
361
367
|
["git-staged-msg", "gitWorkflow"],
|
|
362
368
|
["release-npm", "releaseNpm"],
|
|
363
369
|
["release-aur", "releaseAur"],
|
|
370
|
+
["skills", "tuiSkillsCommand"],
|
|
371
|
+
["tools", "tuiToolsCommand"],
|
|
364
372
|
["stats", "statsCommand"],
|
|
365
373
|
["git-footer-refresh", "gitFooterStatus"],
|
|
366
374
|
["todo-progress-status", "todoProgressWidget"],
|
|
@@ -368,6 +376,7 @@ const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
|
368
376
|
const HIDDEN_COMMAND_NAMES = new Set(["webui-tree-navigate", "webui-helper"]);
|
|
369
377
|
const NATIVE_SELECTOR_COMMANDS = new Set(["model", "settings", "theme", "fork", "clone", "resume", "tree", "login", "logout", "scoped-models", "tools", "skills"]);
|
|
370
378
|
const optionalFeatureInstallInProgress = new Set();
|
|
379
|
+
const gitFooterPayloadRefreshInFlightByTab = new Set();
|
|
371
380
|
|
|
372
381
|
function createGitWorkflowState() {
|
|
373
382
|
return {
|
|
@@ -967,6 +976,92 @@ async function copyText(text) {
|
|
|
967
976
|
if (!copied) throw new Error("Clipboard copy failed");
|
|
968
977
|
}
|
|
969
978
|
|
|
979
|
+
function messageCopyFallbackText(body) {
|
|
980
|
+
return String(body?.innerText || body?.textContent || "").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function messageCopyText(message, body = null) {
|
|
984
|
+
if (!message) return messageCopyFallbackText(body);
|
|
985
|
+
if (message.role === "assistant") {
|
|
986
|
+
const content = message.content === undefined || message.content === null ? "" : textFromContent(message.content);
|
|
987
|
+
const text = stripTodoProgressLines(content).trimEnd();
|
|
988
|
+
return text || messageCopyFallbackText(body);
|
|
989
|
+
}
|
|
990
|
+
if (message.role === "bashExecution") return stripAnsi([`$ ${message.command || ""}`, message.output || ""].filter(Boolean).join("\n\n")).trimEnd();
|
|
991
|
+
if (message.role === "compactionSummary") return String(message.summary || "Context was compacted.").trimEnd();
|
|
992
|
+
if (message.role === "toolResult") {
|
|
993
|
+
const content = message.content === undefined || message.content === null ? "" : textFromContent(message.content);
|
|
994
|
+
return stripAnsi(content).trimEnd() || messageCopyFallbackText(body);
|
|
995
|
+
}
|
|
996
|
+
if (message.role === "toolExecution") {
|
|
997
|
+
const tool = normalizeToolExecution(message);
|
|
998
|
+
const hasArgs = tool.args && Object.keys(tool.args).length > 0;
|
|
999
|
+
const sections = [`tool: ${tool.name}`];
|
|
1000
|
+
if (hasArgs) sections.push(`arguments:\n${JSON.stringify(tool.args, null, 2)}`);
|
|
1001
|
+
if (tool.text) sections.push(`${tool.isPartial ? "live output" : "output"}:\n${tool.text}`);
|
|
1002
|
+
if (tool.details?.fullOutputPath) sections.push(`full output: ${tool.details.fullOutputPath}`);
|
|
1003
|
+
return sections.join("\n\n").trimEnd() || messageCopyFallbackText(body);
|
|
1004
|
+
}
|
|
1005
|
+
if (message.role === "thinking") return visibleThinkingText(message.thinking || textFromContent(message.content)).trimEnd() || messageCopyFallbackText(body);
|
|
1006
|
+
if (message.role === "toolCall") return JSON.stringify(message.arguments ?? message.content ?? {}, null, 2);
|
|
1007
|
+
if (message.role === "assistantEvent") {
|
|
1008
|
+
return (typeof message.content === "string" ? message.content : JSON.stringify(message.content ?? {}, null, 2)).trimEnd();
|
|
1009
|
+
}
|
|
1010
|
+
const content = message.content === undefined || message.content === null ? "" : textFromContent(message.content);
|
|
1011
|
+
return stripAnsi(content).trimEnd() || messageCopyFallbackText(body);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function setMessageCopyButtonState(button, copied) {
|
|
1015
|
+
clearTimeout(button._messageCopyResetTimer);
|
|
1016
|
+
button.classList.toggle("copied", copied);
|
|
1017
|
+
const icon = button.querySelector(".message-copy-icon");
|
|
1018
|
+
if (icon) icon.textContent = copied ? "✓" : "⧉";
|
|
1019
|
+
button.title = copied ? "Copied" : "Copy message";
|
|
1020
|
+
button.setAttribute("aria-label", button.title);
|
|
1021
|
+
if (copied) {
|
|
1022
|
+
button._messageCopyResetTimer = setTimeout(() => setMessageCopyButtonState(button, false), 1400);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
async function copyMessageBubble(button) {
|
|
1027
|
+
const bubble = button.closest(".message");
|
|
1028
|
+
const body = bubble?._copyBody || bubble?.querySelector(":scope > .message-body, :scope > .message-collapse > .message-body");
|
|
1029
|
+
const text = messageCopyText(bubble?._copyMessage, body);
|
|
1030
|
+
if (!text.trim()) {
|
|
1031
|
+
addEvent("message has no text to copy", "warn");
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
button.disabled = true;
|
|
1035
|
+
try {
|
|
1036
|
+
await copyText(text);
|
|
1037
|
+
setMessageCopyButtonState(button, true);
|
|
1038
|
+
} catch (error) {
|
|
1039
|
+
addEvent(`message copy failed: ${error.message || String(error)}`, "warn");
|
|
1040
|
+
} finally {
|
|
1041
|
+
button.disabled = false;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function attachMessageCopyButton(bubble, message, body) {
|
|
1046
|
+
if (!bubble || !body) return null;
|
|
1047
|
+
bubble._copyMessage = message;
|
|
1048
|
+
bubble._copyBody = body;
|
|
1049
|
+
const existing = bubble.querySelector(":scope > .message-copy-button");
|
|
1050
|
+
if (existing) return existing;
|
|
1051
|
+
const button = make("button", "message-copy-button");
|
|
1052
|
+
button.type = "button";
|
|
1053
|
+
button.append(make("span", "message-copy-icon", "⧉"));
|
|
1054
|
+
setMessageCopyButtonState(button, false);
|
|
1055
|
+
button.addEventListener("click", (event) => {
|
|
1056
|
+
event.preventDefault();
|
|
1057
|
+
event.stopPropagation();
|
|
1058
|
+
copyMessageBubble(button);
|
|
1059
|
+
});
|
|
1060
|
+
bubble.classList.add("has-copy-action");
|
|
1061
|
+
bubble.append(button);
|
|
1062
|
+
return button;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
970
1065
|
function triggerNativeDownload(download) {
|
|
971
1066
|
const url = String(download?.url || "").trim();
|
|
972
1067
|
if (!url) return false;
|
|
@@ -1790,6 +1885,10 @@ function setOptionalFeatureDisabled(featureId, disabled) {
|
|
|
1790
1885
|
if (!OPTIONAL_FEATURE_BY_ID.has(featureId)) return;
|
|
1791
1886
|
if (disabled) disabledOptionalFeatures.add(featureId);
|
|
1792
1887
|
else disabledOptionalFeatures.delete(featureId);
|
|
1888
|
+
if (featureId === "gitFooterStatus") {
|
|
1889
|
+
statusEntries.delete(GIT_FOOTER_WEBUI_STATUS_KEY);
|
|
1890
|
+
clearGitFooterWebuiPayloadCache();
|
|
1891
|
+
}
|
|
1793
1892
|
storeDisabledOptionalFeatures();
|
|
1794
1893
|
renderOptionalFeatureDependentDisplays();
|
|
1795
1894
|
const tabContext = activeTabContext();
|
|
@@ -2405,9 +2504,6 @@ function resetActiveTabUi() {
|
|
|
2405
2504
|
latestStats = null;
|
|
2406
2505
|
latestWorkspace = null;
|
|
2407
2506
|
latestMessages = [];
|
|
2408
|
-
currentRunStartedAt = null;
|
|
2409
|
-
currentRunStreamChars = 0;
|
|
2410
|
-
latestTokPerSecond = null;
|
|
2411
2507
|
clearRunIndicatorActivity({ render: false });
|
|
2412
2508
|
statusEntries.clear();
|
|
2413
2509
|
widgets.clear();
|
|
@@ -3232,7 +3328,8 @@ function formatStatusEntry(key, value) {
|
|
|
3232
3328
|
const cleanKey = cleanStatusText(key);
|
|
3233
3329
|
const cleanValue = cleanStatusText(value);
|
|
3234
3330
|
if (!cleanValue) return "";
|
|
3235
|
-
if (cleanKey === "git-footer" && !isOptionalFeatureEnabled("gitFooterStatus")) return "";
|
|
3331
|
+
if ((cleanKey === "git-footer" || cleanKey === GIT_FOOTER_WEBUI_STATUS_KEY) && !isOptionalFeatureEnabled("gitFooterStatus")) return "";
|
|
3332
|
+
if (cleanKey === GIT_FOOTER_WEBUI_STATUS_KEY) return "";
|
|
3236
3333
|
if (cleanKey === "plan-mode") return `Plan: ${cleanValue}`;
|
|
3237
3334
|
if (cleanKey === "extension") return cleanValue;
|
|
3238
3335
|
return `${cleanKey}: ${cleanValue}`;
|
|
@@ -3243,23 +3340,50 @@ function shortModelLabel(model) {
|
|
|
3243
3340
|
return `(${model.provider}) ${model.id}`;
|
|
3244
3341
|
}
|
|
3245
3342
|
|
|
3246
|
-
function
|
|
3247
|
-
const
|
|
3248
|
-
if (!
|
|
3249
|
-
const
|
|
3250
|
-
|
|
3251
|
-
if (abs >= 1_000_000_000) return `${sign}${(abs / 1_000_000_000).toFixed(abs >= 10_000_000_000 ? 0 : 1)}B`;
|
|
3252
|
-
if (abs >= 1_000_000) return `${sign}${(abs / 1_000_000).toFixed(abs >= 10_000_000 ? 0 : 1)}M`;
|
|
3253
|
-
if (abs >= 1_000) return `${sign}${(abs / 1_000).toFixed(abs >= 10_000 ? 0 : 1)}k`;
|
|
3254
|
-
return `${Math.round(n)}`;
|
|
3343
|
+
function footerModelLine(model = currentState?.model, thinkingLevel = currentState?.thinkingLevel) {
|
|
3344
|
+
const label = shortModelLabel(model);
|
|
3345
|
+
if (!model?.reasoning) return label;
|
|
3346
|
+
const thinking = thinkingLevel === "off" ? "thinking off" : thinkingLevel || "?";
|
|
3347
|
+
return `${label} • ${thinking}`;
|
|
3255
3348
|
}
|
|
3256
3349
|
|
|
3257
|
-
function
|
|
3258
|
-
const n = Number(value);
|
|
3259
|
-
if (
|
|
3260
|
-
if (n <
|
|
3261
|
-
if (n <
|
|
3262
|
-
return
|
|
3350
|
+
function formatFooterTokenCount(value) {
|
|
3351
|
+
const n = Math.max(0, Number(value) || 0);
|
|
3352
|
+
if (n < 1000) return `${Math.round(n)}`;
|
|
3353
|
+
if (n < 10000) return `${(n / 1000).toFixed(1)}k`;
|
|
3354
|
+
if (n < 1000000) return `${Math.round(n / 1000)}k`;
|
|
3355
|
+
if (n < 10000000) return `${(n / 1000000).toFixed(1)}M`;
|
|
3356
|
+
return `${Math.round(n / 1000000)}M`;
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
function footerCostAuthLabel() {
|
|
3360
|
+
const provider = currentState?.model?.provider || "";
|
|
3361
|
+
return /codex|copilot|chatgpt/i.test(provider) ? "sub" : "api";
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
function footerStatsTokensDisplay(stats = latestStats) {
|
|
3365
|
+
const tokens = stats?.tokens;
|
|
3366
|
+
if (!tokens) return "";
|
|
3367
|
+
return `↑${formatFooterTokenCount(tokens.input)} ↓${formatFooterTokenCount(tokens.output)}`;
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
function footerStatsCostDisplay(stats = latestStats) {
|
|
3371
|
+
if (!stats) return "";
|
|
3372
|
+
return `$${Number(stats.cost || 0).toFixed(3)} (${footerCostAuthLabel()})`;
|
|
3373
|
+
}
|
|
3374
|
+
|
|
3375
|
+
function footerStatsContextDisplay(stats = latestStats) {
|
|
3376
|
+
const usage = stats?.contextUsage || currentState?.contextUsage;
|
|
3377
|
+
const contextWindow = usage?.contextWindow ?? currentState?.model?.contextWindow ?? 0;
|
|
3378
|
+
if (!contextWindow) return "";
|
|
3379
|
+
const rawPercent = Number(usage?.percent);
|
|
3380
|
+
const percent = Number.isFinite(rawPercent) ? `${rawPercent.toFixed(1)}%` : "?";
|
|
3381
|
+
const auto = currentState?.autoCompactionEnabled !== false ? " (auto)" : "";
|
|
3382
|
+
return `${percent}/${formatFooterTokenCount(contextWindow)}${auto}`;
|
|
3383
|
+
}
|
|
3384
|
+
|
|
3385
|
+
function fallbackFooterStats() {
|
|
3386
|
+
return [footerStatsTokensDisplay(), footerStatsCostDisplay(), footerStatsContextDisplay()].filter(Boolean);
|
|
3263
3387
|
}
|
|
3264
3388
|
|
|
3265
3389
|
function formatDuration(ms) {
|
|
@@ -3290,32 +3414,10 @@ function textFromContent(content) {
|
|
|
3290
3414
|
.join("\n");
|
|
3291
3415
|
}
|
|
3292
3416
|
|
|
3293
|
-
function
|
|
3294
|
-
let chars = 0;
|
|
3295
|
-
for (const message of messages || []) {
|
|
3296
|
-
chars += textFromContent(message.content).length;
|
|
3297
|
-
if (message.role === "toolResult") chars += textFromContent(message.content).length;
|
|
3298
|
-
if (message.role === "bashExecution") chars += String(message.command || "").length + String(message.output || "").length;
|
|
3299
|
-
chars += 16;
|
|
3300
|
-
}
|
|
3301
|
-
return Math.round(chars / 4);
|
|
3302
|
-
}
|
|
3303
|
-
|
|
3304
|
-
function estimatePiTokens() {
|
|
3305
|
-
const contextTokens = latestStats?.contextUsage?.tokens;
|
|
3306
|
-
if (!Number.isFinite(Number(contextTokens))) return null;
|
|
3307
|
-
return Math.max(0, Number(contextTokens) - estimateMessageTokens(latestMessages));
|
|
3308
|
-
}
|
|
3309
|
-
|
|
3310
|
-
function subscriptionSuffix() {
|
|
3311
|
-
const provider = currentState?.model?.provider || "";
|
|
3312
|
-
return /codex|copilot|chatgpt/i.test(provider) ? "sub" : "metered";
|
|
3313
|
-
}
|
|
3314
|
-
|
|
3315
|
-
function footerMetric(icon, label, value, tone = "") {
|
|
3417
|
+
function footerMetric(icon, label, value, tone = "", options = {}) {
|
|
3316
3418
|
const node = make("span", `footer-metric ${tone}`.trim());
|
|
3317
3419
|
node.append(make("span", "footer-metric-icon", icon), make("span", "footer-metric-label", label), make("span", "footer-metric-value", value));
|
|
3318
|
-
node.title = `${label}: ${value}`;
|
|
3420
|
+
node.title = options.title || `${label}: ${value}`;
|
|
3319
3421
|
return node;
|
|
3320
3422
|
}
|
|
3321
3423
|
|
|
@@ -3342,7 +3444,7 @@ function contextUsageActiveColor(percent) {
|
|
|
3342
3444
|
|
|
3343
3445
|
function applyFooterContextUsage(node, contextUsage) {
|
|
3344
3446
|
node.classList.add("footer-context-card");
|
|
3345
|
-
const percent =
|
|
3447
|
+
const percent = typeof contextUsage?.percent === "number" ? contextUsage.percent : Number.NaN;
|
|
3346
3448
|
if (Number.isFinite(percent)) {
|
|
3347
3449
|
const clampedPercent = Math.min(100, Math.max(0, percent));
|
|
3348
3450
|
const activeColor = contextUsageActiveColor(clampedPercent);
|
|
@@ -3366,6 +3468,223 @@ function footerMeta(label, value, className = "", options = {}) {
|
|
|
3366
3468
|
return node;
|
|
3367
3469
|
}
|
|
3368
3470
|
|
|
3471
|
+
const FOOTER_PAYLOAD_TONES = new Set(["pink", "blue", "mauve", "yellow", "green", "teal"]);
|
|
3472
|
+
const FOOTER_META_CLASS_BY_KEY = new Map([
|
|
3473
|
+
["cwd", "footer-workspace"],
|
|
3474
|
+
["git", "footer-branch"],
|
|
3475
|
+
["git-state", "footer-git-state"],
|
|
3476
|
+
["sync", "footer-sync"],
|
|
3477
|
+
["changes", "footer-changes"],
|
|
3478
|
+
["git-extra", "footer-git-extra"],
|
|
3479
|
+
["context", "footer-context"],
|
|
3480
|
+
["model", "footer-model"],
|
|
3481
|
+
]);
|
|
3482
|
+
|
|
3483
|
+
function cleanFooterPayloadText(value, fallback = "") {
|
|
3484
|
+
const text = cleanStatusText(value).slice(0, 240);
|
|
3485
|
+
return text || fallback;
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
function normalizeFooterPayloadChip(value, index) {
|
|
3489
|
+
if (!value || typeof value !== "object") return null;
|
|
3490
|
+
const key = cleanFooterPayloadText(value.key, `item-${index}`).replace(/[^a-z0-9_.:-]/gi, "-").slice(0, 64) || `item-${index}`;
|
|
3491
|
+
const label = cleanFooterPayloadText(value.label, key).slice(0, 32);
|
|
3492
|
+
const chip = {
|
|
3493
|
+
key,
|
|
3494
|
+
label,
|
|
3495
|
+
value: cleanFooterPayloadText(value.value, "—"),
|
|
3496
|
+
icon: cleanFooterPayloadText(value.icon, "•").slice(0, 8),
|
|
3497
|
+
tone: FOOTER_PAYLOAD_TONES.has(value.tone) ? value.tone : "",
|
|
3498
|
+
title: cleanFooterPayloadText(value.title, ""),
|
|
3499
|
+
};
|
|
3500
|
+
if (value.contextUsage && typeof value.contextUsage === "object") {
|
|
3501
|
+
const percent = typeof value.contextUsage.percent === "number" ? value.contextUsage.percent : Number.NaN;
|
|
3502
|
+
const contextWindow = Number(value.contextUsage.contextWindow);
|
|
3503
|
+
chip.contextUsage = {
|
|
3504
|
+
percent: Number.isFinite(percent) ? percent : null,
|
|
3505
|
+
contextWindow: Number.isFinite(contextWindow) ? contextWindow : 0,
|
|
3506
|
+
};
|
|
3507
|
+
}
|
|
3508
|
+
return chip;
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
function currentGitFooterCacheCwd(tabId = activeTabId) {
|
|
3512
|
+
const tab = tabs.find((item) => item.id === tabId) || activeTab();
|
|
3513
|
+
return latestWorkspace?.cwd || tab?.cwd || "";
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
function parseGitFooterWebuiPayloadRaw(raw) {
|
|
3517
|
+
if (!raw) return null;
|
|
3518
|
+
try {
|
|
3519
|
+
const parsed = JSON.parse(raw);
|
|
3520
|
+
if (!parsed || parsed.type !== GIT_FOOTER_WEBUI_PAYLOAD_TYPE || parsed.version !== GIT_FOOTER_WEBUI_PAYLOAD_VERSION) return null;
|
|
3521
|
+
const main = Array.isArray(parsed.main) ? parsed.main.map(normalizeFooterPayloadChip).filter(Boolean).slice(0, 8) : [];
|
|
3522
|
+
const meta = Array.isArray(parsed.meta) ? parsed.meta.map(normalizeFooterPayloadChip).filter(Boolean).slice(0, 10) : [];
|
|
3523
|
+
if (!main.length && !meta.length) return null;
|
|
3524
|
+
return { main, meta };
|
|
3525
|
+
} catch {
|
|
3526
|
+
return null;
|
|
3527
|
+
}
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
function readCachedGitFooterWebuiPayloadRaw() {
|
|
3531
|
+
try {
|
|
3532
|
+
const cached = JSON.parse(localStorage.getItem(GIT_FOOTER_WEBUI_PAYLOAD_CACHE_KEY) || "null");
|
|
3533
|
+
if (!cached || typeof cached.raw !== "string") return null;
|
|
3534
|
+
const cachedCwd = typeof cached.cwd === "string" ? cached.cwd : "";
|
|
3535
|
+
const currentCwd = currentGitFooterCacheCwd();
|
|
3536
|
+
if (cachedCwd && currentCwd && cachedCwd !== currentCwd) return null;
|
|
3537
|
+
return cached.raw;
|
|
3538
|
+
} catch {
|
|
3539
|
+
return null;
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
function cacheGitFooterWebuiPayload(raw, tabId = activeTabId) {
|
|
3544
|
+
if (!parseGitFooterWebuiPayloadRaw(raw)) return;
|
|
3545
|
+
try {
|
|
3546
|
+
localStorage.setItem(GIT_FOOTER_WEBUI_PAYLOAD_CACHE_KEY, JSON.stringify({
|
|
3547
|
+
raw,
|
|
3548
|
+
cwd: currentGitFooterCacheCwd(tabId),
|
|
3549
|
+
savedAt: Date.now(),
|
|
3550
|
+
}));
|
|
3551
|
+
} catch {
|
|
3552
|
+
// Cached footer payloads are best-effort; live extension payloads still work.
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
function clearGitFooterWebuiPayloadCache() {
|
|
3557
|
+
try {
|
|
3558
|
+
localStorage.removeItem(GIT_FOOTER_WEBUI_PAYLOAD_CACHE_KEY);
|
|
3559
|
+
} catch {
|
|
3560
|
+
// Ignore storage failures; toggles should still work for this page load.
|
|
3561
|
+
}
|
|
3562
|
+
}
|
|
3563
|
+
|
|
3564
|
+
function parseGitFooterWebuiPayload() {
|
|
3565
|
+
if (isOptionalFeatureDisabled("gitFooterStatus")) return null;
|
|
3566
|
+
|
|
3567
|
+
const livePayload = parseGitFooterWebuiPayloadRaw(statusEntries.get(GIT_FOOTER_WEBUI_STATUS_KEY));
|
|
3568
|
+
if (livePayload) return livePayload;
|
|
3569
|
+
|
|
3570
|
+
const commandsStillLoading = availableCommands.length === 0 && rawAvailableCommands.length === 0;
|
|
3571
|
+
const extensionDetected = hasAvailableCommand("git-footer-refresh") || optionalFeatureAvailability.gitFooterStatus;
|
|
3572
|
+
if (!commandsStillLoading && !extensionDetected) return null;
|
|
3573
|
+
return parseGitFooterWebuiPayloadRaw(readCachedGitFooterWebuiPayloadRaw());
|
|
3574
|
+
}
|
|
3575
|
+
|
|
3576
|
+
function footerMetaClassForPayload(chip) {
|
|
3577
|
+
const base = FOOTER_META_CLASS_BY_KEY.get(chip.key) || "footer-extension-meta";
|
|
3578
|
+
const toneClass = chip.tone ? ` tone-${chip.tone}` : "";
|
|
3579
|
+
return `${base}${toneClass}`.trim();
|
|
3580
|
+
}
|
|
3581
|
+
|
|
3582
|
+
function footerTuiItem(value, className = "", options = {}) {
|
|
3583
|
+
const text = cleanStatusText(value);
|
|
3584
|
+
const isAction = typeof options.onClick === "function";
|
|
3585
|
+
const node = make(isAction ? "button" : "span", `footer-tui-item ${className}${isAction ? " footer-tui-action" : ""}`.trim(), text);
|
|
3586
|
+
if (isAction) {
|
|
3587
|
+
node.type = "button";
|
|
3588
|
+
node.addEventListener("click", options.onClick);
|
|
3589
|
+
}
|
|
3590
|
+
if (options.title) node.title = options.title;
|
|
3591
|
+
return node;
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
function renderTuiFooterLine({ cwd, cwdTitle, message = "", stats = [], model = "" } = {}) {
|
|
3595
|
+
const tab = activeTab();
|
|
3596
|
+
const line = make("div", "footer-line footer-line-tui");
|
|
3597
|
+
line.append(footerTuiItem(cwd || "loading…", "footer-tui-cwd", tab ? {
|
|
3598
|
+
onClick: changeActiveTabCwd,
|
|
3599
|
+
title: cwdTitle || `Change cwd for ${tab.title}: ${cwd}`,
|
|
3600
|
+
} : { title: cwdTitle }));
|
|
3601
|
+
if (message) line.append(footerTuiItem(message, "footer-tui-status"));
|
|
3602
|
+
for (const stat of stats.filter(Boolean)) line.append(footerTuiItem(stat, "footer-tui-stat"));
|
|
3603
|
+
if (model) {
|
|
3604
|
+
line.append(make("span", "footer-tui-spacer"));
|
|
3605
|
+
line.append(footerTuiItem(model, "footer-tui-model", {
|
|
3606
|
+
onClick: () => setFooterModelPickerOpen(!footerModelPickerOpen),
|
|
3607
|
+
title: `Change scoped model: ${model}`,
|
|
3608
|
+
}));
|
|
3609
|
+
}
|
|
3610
|
+
return line;
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
function renderGitFooterPayloadMetric(chip) {
|
|
3614
|
+
const node = footerMetric(chip.icon || "•", chip.label, chip.value, chip.tone ? `tone-${chip.tone}` : "", { title: chip.title || undefined });
|
|
3615
|
+
return chip.contextUsage ? applyFooterContextUsage(node, chip.contextUsage) : node;
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
function renderGitFooterPayloadMeta(chip, tab) {
|
|
3619
|
+
const options = { title: chip.title || undefined };
|
|
3620
|
+
if (chip.key === "cwd" && tab) {
|
|
3621
|
+
options.onClick = changeActiveTabCwd;
|
|
3622
|
+
options.title = chip.title || `Change cwd for ${tab.title}: ${chip.value}`;
|
|
3623
|
+
} else if (chip.key === "model") {
|
|
3624
|
+
options.onClick = () => setFooterModelPickerOpen(!footerModelPickerOpen);
|
|
3625
|
+
options.title = chip.title || `Change scoped model: ${chip.value}`;
|
|
3626
|
+
}
|
|
3627
|
+
const node = footerMeta(chip.label, chip.value, footerMetaClassForPayload(chip), options);
|
|
3628
|
+
return chip.contextUsage ? applyFooterContextUsage(node, chip.contextUsage) : node;
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
function renderGitFooterPayload(payload) {
|
|
3632
|
+
const tab = activeTab();
|
|
3633
|
+
elements.statusBar.replaceChildren();
|
|
3634
|
+
elements.statusBar.classList.remove("statusbar-tui-footer");
|
|
3635
|
+
elements.statusBar.classList.add("statusbar-git-footer");
|
|
3636
|
+
document.body.classList.toggle("footer-model-picker-open", footerModelPickerOpen);
|
|
3637
|
+
|
|
3638
|
+
const row1 = make("div", "footer-line footer-line-main");
|
|
3639
|
+
row1.append(...payload.main.map(renderGitFooterPayloadMetric));
|
|
3640
|
+
|
|
3641
|
+
const footerToggle = make("button", "footer-details-toggle", mobileFooterExpanded ? "Less" : "Details");
|
|
3642
|
+
footerToggle.type = "button";
|
|
3643
|
+
footerToggle.setAttribute("aria-expanded", mobileFooterExpanded ? "true" : "false");
|
|
3644
|
+
footerToggle.addEventListener("click", () => setMobileFooterExpanded(!mobileFooterExpanded));
|
|
3645
|
+
|
|
3646
|
+
const row2 = make("div", "footer-line footer-line-meta");
|
|
3647
|
+
row2.append(...payload.meta.map((chip) => renderGitFooterPayloadMeta(chip, tab)), footerToggle);
|
|
3648
|
+
|
|
3649
|
+
elements.statusBar.append(row1, row2);
|
|
3650
|
+
if (footerModelPickerOpen) elements.statusBar.append(renderFooterModelPicker());
|
|
3651
|
+
setMobileFooterExpanded(mobileFooterExpanded);
|
|
3652
|
+
updateFooterModelPickerPosition();
|
|
3653
|
+
}
|
|
3654
|
+
|
|
3655
|
+
function gitFooterFallbackMessage() {
|
|
3656
|
+
if (isOptionalFeatureDisabled("gitFooterStatus")) return "";
|
|
3657
|
+
const tabContext = activeTabContext();
|
|
3658
|
+
const commandsStillLoading = availableCommands.length === 0 && rawAvailableCommands.length === 0;
|
|
3659
|
+
const footerRefreshPending = tabContext.tabId ? gitFooterPayloadRefreshInFlightByTab.has(tabContext.tabId) : false;
|
|
3660
|
+
const extensionDetected = hasAvailableCommand("git-footer-refresh") || optionalFeatureAvailability.gitFooterStatus;
|
|
3661
|
+
return commandsStillLoading || footerRefreshPending || extensionDetected
|
|
3662
|
+
? "Loading git footer status…"
|
|
3663
|
+
: "Git footer status extension unavailable";
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
function renderMinimalFooter() {
|
|
3667
|
+
const tab = activeTab();
|
|
3668
|
+
const workspaceLabel = latestWorkspace?.displayCwd || (tab?.cwd ? normalizeDisplayPath(tab.cwd) : "loading…");
|
|
3669
|
+
const modelLine = footerModelLine();
|
|
3670
|
+
const footerMessage = gitFooterFallbackMessage();
|
|
3671
|
+
|
|
3672
|
+
elements.statusBar.replaceChildren();
|
|
3673
|
+
elements.statusBar.classList.remove("statusbar-git-footer");
|
|
3674
|
+
elements.statusBar.classList.add("statusbar-tui-footer");
|
|
3675
|
+
document.body.classList.toggle("footer-model-picker-open", footerModelPickerOpen);
|
|
3676
|
+
elements.statusBar.append(renderTuiFooterLine({
|
|
3677
|
+
cwd: workspaceLabel,
|
|
3678
|
+
cwdTitle: tab ? `Change cwd for ${tab.title}: ${workspaceLabel}` : undefined,
|
|
3679
|
+
message: footerMessage,
|
|
3680
|
+
stats: fallbackFooterStats(),
|
|
3681
|
+
model: modelLine,
|
|
3682
|
+
}));
|
|
3683
|
+
if (footerModelPickerOpen) elements.statusBar.append(renderFooterModelPicker());
|
|
3684
|
+
setMobileFooterExpanded(false);
|
|
3685
|
+
updateFooterModelPickerPosition();
|
|
3686
|
+
}
|
|
3687
|
+
|
|
3369
3688
|
function setFooterModelPickerOpen(open) {
|
|
3370
3689
|
footerModelPickerOpen = !!open;
|
|
3371
3690
|
if (footerModelPickerOpen && isMobileView()) {
|
|
@@ -3696,62 +4015,12 @@ async function changeActiveTabCwd() {
|
|
|
3696
4015
|
}
|
|
3697
4016
|
|
|
3698
4017
|
function renderFooter() {
|
|
3699
|
-
const
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
: latestTokPerSecond;
|
|
3706
|
-
const speedLabel = Number.isFinite(speed) ? `${speed.toFixed(1)} tok/s` : "-- tok/s";
|
|
3707
|
-
const contextLabel = contextUsage?.contextWindow
|
|
3708
|
-
? `${contextUsage.percent !== null && contextUsage.percent !== undefined ? `${Number(contextUsage.percent).toFixed(1)}% / ` : ""}${formatTokenCount(contextUsage.contextWindow)}`
|
|
3709
|
-
: "?";
|
|
3710
|
-
|
|
3711
|
-
const tab = activeTab();
|
|
3712
|
-
const git = latestWorkspace?.git;
|
|
3713
|
-
const branchLabel = git?.isRepo ? git.branch || "detached" : "no repo";
|
|
3714
|
-
const changeLabel = git?.isRepo ? `✎ ${git.changed ?? 0} ◌ ${git.untracked ?? 0}` : "no git";
|
|
3715
|
-
const workspaceLabel = latestWorkspace?.displayCwd || (tab?.cwd ? normalizeDisplayPath(tab.cwd) : "loading…");
|
|
3716
|
-
const runtime = latestWorkspace?.uptimeMs ? formatDuration(latestWorkspace.uptimeMs) : "--";
|
|
3717
|
-
const modelLine = `${shortModelLabel(currentState?.model)} · ${currentState?.thinkingLevel || "?"}`;
|
|
3718
|
-
|
|
3719
|
-
elements.statusBar.replaceChildren();
|
|
3720
|
-
document.body.classList.toggle("footer-model-picker-open", footerModelPickerOpen);
|
|
3721
|
-
const row1 = make("div", "footer-line footer-line-main");
|
|
3722
|
-
row1.append(
|
|
3723
|
-
footerMetric("🪙", "tokens", `↑ ${formatTokenCount(tokens.input ?? 0)} ↓ ${formatTokenCount(tokens.output ?? 0)}`, "tone-pink"),
|
|
3724
|
-
footerMetric("💾", "cache", `R ${formatTokenCount(tokens.cacheRead ?? 0)}${tokens.cacheWrite ? ` W ${formatTokenCount(tokens.cacheWrite)}` : ""}`, "tone-blue"),
|
|
3725
|
-
footerMetric("π", "pi", piTokens === null ? "-- tok" : `~${formatTokenCount(piTokens)} tok`, "tone-mauve"),
|
|
3726
|
-
footerMetric("⚡", "speed", speedLabel, "tone-yellow"),
|
|
3727
|
-
footerMetric("💸", subscriptionSuffix(), formatCost(stats?.cost ?? 0), "tone-green"),
|
|
3728
|
-
applyFooterContextUsage(footerMetric("🧠", "context", contextLabel, "tone-teal"), contextUsage),
|
|
3729
|
-
);
|
|
3730
|
-
const footerToggle = make("button", "footer-details-toggle", mobileFooterExpanded ? "Less" : "Details");
|
|
3731
|
-
footerToggle.type = "button";
|
|
3732
|
-
footerToggle.setAttribute("aria-expanded", mobileFooterExpanded ? "true" : "false");
|
|
3733
|
-
footerToggle.addEventListener("click", () => setMobileFooterExpanded(!mobileFooterExpanded));
|
|
3734
|
-
|
|
3735
|
-
const row2 = make("div", "footer-line footer-line-meta");
|
|
3736
|
-
row2.append(
|
|
3737
|
-
footerMeta("cwd", workspaceLabel, "footer-workspace", tab ? {
|
|
3738
|
-
onClick: changeActiveTabCwd,
|
|
3739
|
-
title: `Change cwd for ${tab.title}: ${workspaceLabel}`,
|
|
3740
|
-
} : {}),
|
|
3741
|
-
footerMeta("git", branchLabel, "footer-branch"),
|
|
3742
|
-
footerMeta("changes", changeLabel, "footer-changes"),
|
|
3743
|
-
footerMeta("runtime", `⏱ ${runtime} · Agent`, "footer-runtime"),
|
|
3744
|
-
applyFooterContextUsage(footerMeta("context", contextLabel, "footer-context"), contextUsage),
|
|
3745
|
-
footerMeta("model", modelLine, "footer-model", {
|
|
3746
|
-
onClick: () => setFooterModelPickerOpen(!footerModelPickerOpen),
|
|
3747
|
-
title: `Change scoped model: ${modelLine}`,
|
|
3748
|
-
}),
|
|
3749
|
-
footerToggle,
|
|
3750
|
-
);
|
|
3751
|
-
elements.statusBar.append(row1, row2);
|
|
3752
|
-
if (footerModelPickerOpen) elements.statusBar.append(renderFooterModelPicker());
|
|
3753
|
-
setMobileFooterExpanded(mobileFooterExpanded);
|
|
3754
|
-
updateFooterModelPickerPosition();
|
|
4018
|
+
const gitFooterPayload = parseGitFooterWebuiPayload();
|
|
4019
|
+
if (gitFooterPayload) {
|
|
4020
|
+
renderGitFooterPayload(gitFooterPayload);
|
|
4021
|
+
return;
|
|
4022
|
+
}
|
|
4023
|
+
renderMinimalFooter();
|
|
3755
4024
|
}
|
|
3756
4025
|
|
|
3757
4026
|
function scheduleRefreshMessages(delay = 120, tabContext = activeTabContext()) {
|
|
@@ -6284,6 +6553,7 @@ function updateLiveToolCard(bubble, message) {
|
|
|
6284
6553
|
const header = bubble.querySelector(":scope > .message-header");
|
|
6285
6554
|
const body = bubble.querySelector(":scope > .message-body");
|
|
6286
6555
|
if (!body) return false;
|
|
6556
|
+
attachMessageCopyButton(bubble, message, body);
|
|
6287
6557
|
applyToolExecutionBubbleState(bubble, message);
|
|
6288
6558
|
const role = header?.querySelector(".message-role");
|
|
6289
6559
|
if (role) role.textContent = messageTitle(message);
|
|
@@ -6469,6 +6739,7 @@ function appendMessage(message, { streaming = false, messageIndex = -1, transien
|
|
|
6469
6739
|
} else {
|
|
6470
6740
|
bubble.append(header, body);
|
|
6471
6741
|
}
|
|
6742
|
+
attachMessageCopyButton(bubble, message, body);
|
|
6472
6743
|
if (!streaming && !transient) renderActionFeedbackControls(bubble, message, messageIndex);
|
|
6473
6744
|
appendChatMessageBubble(bubble);
|
|
6474
6745
|
return { bubble, body };
|
|
@@ -6943,6 +7214,13 @@ function setPublishMenuOpen(open) {
|
|
|
6943
7214
|
elements.publishButton.parentElement?.classList.toggle("open", publishMenuOpen);
|
|
6944
7215
|
}
|
|
6945
7216
|
|
|
7217
|
+
function setNativeCommandMenuOpen(open) {
|
|
7218
|
+
nativeCommandMenuOpen = !!open;
|
|
7219
|
+
elements.nativeCommandMenuButton.setAttribute("aria-expanded", nativeCommandMenuOpen ? "true" : "false");
|
|
7220
|
+
elements.nativeCommandMenuButton.classList.toggle("menu-open", nativeCommandMenuOpen);
|
|
7221
|
+
elements.nativeCommandMenuButton.parentElement?.classList.toggle("open", nativeCommandMenuOpen);
|
|
7222
|
+
}
|
|
7223
|
+
|
|
6946
7224
|
function optionalFeatureIdForCommand(name) {
|
|
6947
7225
|
if (OPTIONAL_COMMAND_FEATURES.has(name)) return OPTIONAL_COMMAND_FEATURES.get(name);
|
|
6948
7226
|
if (name === "release-toggle" || name === "release-abort" || name === "release-npm-logs") return "releaseNpm";
|
|
@@ -7010,16 +7288,37 @@ function resetOptionalFeatureAvailability() {
|
|
|
7010
7288
|
renderOptionalFeatureControls();
|
|
7011
7289
|
}
|
|
7012
7290
|
|
|
7291
|
+
function requestGitFooterWebuiPayload(tabContext = activeTabContext()) {
|
|
7292
|
+
if (!tabContext.tabId || isOptionalFeatureDisabled("gitFooterStatus")) return;
|
|
7293
|
+
if (currentState?.isStreaming || currentState?.isCompacting) return;
|
|
7294
|
+
if (!hasAvailableCommand("git-footer-refresh") || statusEntries.has(GIT_FOOTER_WEBUI_STATUS_KEY)) return;
|
|
7295
|
+
if (gitFooterPayloadRefreshInFlightByTab.has(tabContext.tabId)) return;
|
|
7296
|
+
|
|
7297
|
+
gitFooterPayloadRefreshInFlightByTab.add(tabContext.tabId);
|
|
7298
|
+
if (isCurrentTabContext(tabContext)) renderFooter();
|
|
7299
|
+
api("/api/prompt", {
|
|
7300
|
+
method: "POST",
|
|
7301
|
+
body: { message: "/git-footer-refresh --webui-silent", streamingBehavior: "steer" },
|
|
7302
|
+
tabId: tabContext.tabId,
|
|
7303
|
+
}).catch((error) => {
|
|
7304
|
+
if (isCurrentTabContext(tabContext)) addEvent(`git footer payload refresh failed: ${error.message || String(error)}`, "warn");
|
|
7305
|
+
}).finally(() => {
|
|
7306
|
+
gitFooterPayloadRefreshInFlightByTab.delete(tabContext.tabId);
|
|
7307
|
+
if (isCurrentTabContext(tabContext)) renderFooter();
|
|
7308
|
+
});
|
|
7309
|
+
}
|
|
7310
|
+
|
|
7013
7311
|
function updateOptionalFeatureAvailability() {
|
|
7014
7312
|
optionalFeatureAvailability.gitWorkflow = hasAvailableCommand("git-staged-msg");
|
|
7015
7313
|
optionalFeatureAvailability.releaseNpm = hasAvailableCommand("release-npm");
|
|
7016
7314
|
optionalFeatureAvailability.releaseAur = hasAvailableCommand("release-aur");
|
|
7017
7315
|
optionalFeatureAvailability.statsCommand = hasAvailableCommand("stats");
|
|
7018
|
-
optionalFeatureAvailability.gitFooterStatus = hasAvailableCommand("git-footer-refresh") || optionalFeatureAvailability.gitFooterStatus || statusEntries.has("git-footer");
|
|
7316
|
+
optionalFeatureAvailability.gitFooterStatus = hasAvailableCommand("git-footer-refresh") || optionalFeatureAvailability.gitFooterStatus || statusEntries.has("git-footer") || statusEntries.has(GIT_FOOTER_WEBUI_STATUS_KEY);
|
|
7019
7317
|
optionalFeatureAvailability.tuiSkillsCommand = hasLoadedRpcCommand("skills");
|
|
7020
7318
|
optionalFeatureAvailability.todoProgressWidget = hasAvailableCommand("todo-progress-status") || optionalFeatureAvailability.todoProgressWidget || widgets.has("todo-progress");
|
|
7021
7319
|
optionalFeatureAvailability.tuiToolsCommand = hasLoadedRpcCommand("tools");
|
|
7022
7320
|
optionalFeatureAvailability.themeBundle = availableThemes.length > 0;
|
|
7321
|
+
requestGitFooterWebuiPayload();
|
|
7023
7322
|
renderOptionalFeatureControls();
|
|
7024
7323
|
}
|
|
7025
7324
|
|
|
@@ -7096,6 +7395,18 @@ function renderOptionalFeatureControls() {
|
|
|
7096
7395
|
);
|
|
7097
7396
|
if (!hasPublishWorkflow && publishMenuOpen) setPublishMenuOpen(false);
|
|
7098
7397
|
|
|
7398
|
+
const hasNativeCommandMenu = isOptionalFeatureEnabled("tuiSkillsCommand") || isOptionalFeatureEnabled("tuiToolsCommand");
|
|
7399
|
+
elements.nativeSkillsButton.hidden = !isOptionalFeatureEnabled("tuiSkillsCommand");
|
|
7400
|
+
elements.nativeToolsButton.hidden = !isOptionalFeatureEnabled("tuiToolsCommand");
|
|
7401
|
+
const nativeCommandMenuContainer = elements.nativeCommandMenuButton.parentElement;
|
|
7402
|
+
if (nativeCommandMenuContainer) nativeCommandMenuContainer.hidden = !hasNativeCommandMenu;
|
|
7403
|
+
setOptionalControlState(
|
|
7404
|
+
elements.nativeCommandMenuButton,
|
|
7405
|
+
hasNativeCommandMenu,
|
|
7406
|
+
"Slash command menu unavailable: enable/install TUI Skills command and/or TUI Tools command in Optional features.",
|
|
7407
|
+
);
|
|
7408
|
+
if (!hasNativeCommandMenu && nativeCommandMenuOpen) setNativeCommandMenuOpen(false);
|
|
7409
|
+
|
|
7099
7410
|
renderOptionalFeaturePanel();
|
|
7100
7411
|
}
|
|
7101
7412
|
|
|
@@ -7159,6 +7470,23 @@ function runPublishWorkflow(command) {
|
|
|
7159
7470
|
sendPrompt("prompt", command);
|
|
7160
7471
|
}
|
|
7161
7472
|
|
|
7473
|
+
async function runNativeCommandMenu(command) {
|
|
7474
|
+
setComposerActionsOpen(false);
|
|
7475
|
+
setPublishMenuOpen(false);
|
|
7476
|
+
setNativeCommandMenuOpen(false);
|
|
7477
|
+
const commandName = String(command || "").replace(/^\//, "").split(/\s+/)[0].toLowerCase();
|
|
7478
|
+
const featureId = optionalFeatureIdForCommand(commandName);
|
|
7479
|
+
if ((featureId && !isOptionalFeatureEnabled(featureId)) || !hasAvailableCommand(commandName)) {
|
|
7480
|
+
const tabContext = activeTabContext();
|
|
7481
|
+
addEvent(commandUnavailableMessage(commandName), "warn");
|
|
7482
|
+
refreshCommands(tabContext).catch((error) => {
|
|
7483
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
7484
|
+
});
|
|
7485
|
+
return;
|
|
7486
|
+
}
|
|
7487
|
+
await handleNativeSlashSelectorCommand(command);
|
|
7488
|
+
}
|
|
7489
|
+
|
|
7162
7490
|
function slashCommandName(message) {
|
|
7163
7491
|
const match = String(message || "").trim().match(/^\/([^\s]+)$/);
|
|
7164
7492
|
return match ? match[1].toLowerCase() : "";
|
|
@@ -7212,7 +7540,8 @@ function renderNativeLoading(label = "Loading…") {
|
|
|
7212
7540
|
function nativeSelectorMatches(item, query) {
|
|
7213
7541
|
if (!query) return true;
|
|
7214
7542
|
const needle = query.toLowerCase();
|
|
7215
|
-
|
|
7543
|
+
const tags = Array.isArray(item.tags) ? item.tags.map((tag) => tag?.label) : [];
|
|
7544
|
+
return [item.label, item.description, item.meta, item.badge, ...tags]
|
|
7216
7545
|
.filter(Boolean)
|
|
7217
7546
|
.some((value) => String(value).toLowerCase().includes(needle));
|
|
7218
7547
|
}
|
|
@@ -7245,6 +7574,10 @@ function renderNativeSelectorItems(items, { emptyText = "No choices.", onSelect,
|
|
|
7245
7574
|
}
|
|
7246
7575
|
title.append(badge);
|
|
7247
7576
|
}
|
|
7577
|
+
for (const tag of Array.isArray(item.tags) ? item.tags : []) {
|
|
7578
|
+
if (!tag?.label) continue;
|
|
7579
|
+
title.append(make("span", `native-selector-badge${tag.className ? ` ${tag.className}` : ""}`, tag.label));
|
|
7580
|
+
}
|
|
7248
7581
|
const detail = make("span", "native-selector-detail", item.description || "");
|
|
7249
7582
|
const meta = make("span", "native-selector-meta", item.meta || "");
|
|
7250
7583
|
button.append(title);
|
|
@@ -7583,6 +7916,12 @@ function nativeResourceSourceLabel(resource) {
|
|
|
7583
7916
|
return [info.source, info.scope, info.origin].filter(Boolean).join(" · ") || resource?.location || "loaded resource";
|
|
7584
7917
|
}
|
|
7585
7918
|
|
|
7919
|
+
function nativeToolOriginTag(resource) {
|
|
7920
|
+
return resource?.sourceInfo?.source === "builtin"
|
|
7921
|
+
? { label: "Pi Native", className: "native-selector-badge-pi-native" }
|
|
7922
|
+
: { label: "External", className: "native-selector-badge-external" };
|
|
7923
|
+
}
|
|
7924
|
+
|
|
7586
7925
|
function nativeResourceCounts(resources) {
|
|
7587
7926
|
const disabled = resources.filter((resource) => resource.enabled === false).length;
|
|
7588
7927
|
return { total: resources.length, disabled, enabled: resources.length - disabled };
|
|
@@ -7594,19 +7933,23 @@ function nativeResourceFilterMatches(resource, filter) {
|
|
|
7594
7933
|
return true;
|
|
7595
7934
|
}
|
|
7596
7935
|
|
|
7597
|
-
function renderNativeResourceToggles(resources, { savingName, filter = "all", onToggle } = {}) {
|
|
7936
|
+
function renderNativeResourceToggles(resources, { savingName, filter = "all", onToggle, getResourceTag } = {}) {
|
|
7598
7937
|
const filteredResources = resources.filter((resource) => nativeResourceFilterMatches(resource, filter));
|
|
7599
7938
|
const counts = nativeResourceCounts(resources);
|
|
7600
|
-
const items = filteredResources.map((resource) =>
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7939
|
+
const items = filteredResources.map((resource) => {
|
|
7940
|
+
const resourceTag = getResourceTag?.(resource);
|
|
7941
|
+
return {
|
|
7942
|
+
id: resource.name,
|
|
7943
|
+
label: resource.name,
|
|
7944
|
+
description: resource.description || "No description provided.",
|
|
7945
|
+
meta: nativeResourceSourceLabel(resource),
|
|
7946
|
+
badge: resource.enabled === false ? "disabled" : "enabled",
|
|
7947
|
+
badgeClass: resource.enabled === false ? "disabled native-selector-badge-disabled" : "enabled native-selector-badge-enabled",
|
|
7948
|
+
tags: resourceTag ? [resourceTag] : [],
|
|
7949
|
+
disabled: Boolean(savingName),
|
|
7950
|
+
resource,
|
|
7951
|
+
};
|
|
7952
|
+
});
|
|
7610
7953
|
const filterLabel = filter === "enabled" ? "enabled" : filter === "disabled" ? "disabled" : "all";
|
|
7611
7954
|
renderNativeSelectorItems(items, {
|
|
7612
7955
|
emptyText: `No ${filterLabel} entries match this filter.`,
|
|
@@ -7631,7 +7974,7 @@ function renderNativeResourceFilterActions(filter, setFilter, render) {
|
|
|
7631
7974
|
}
|
|
7632
7975
|
|
|
7633
7976
|
async function openNativeToolsSelector() {
|
|
7634
|
-
openNativeCommandDialog({ title: "
|
|
7977
|
+
openNativeCommandDialog({ title: "Tools Setup", message: "Enable or disable tools for the active Pi tab. Changes apply to the next model turn and persist on this session branch.", searchPlaceholder: "Filter tools…" });
|
|
7635
7978
|
renderNativeLoading("Loading tools…");
|
|
7636
7979
|
let tools = [];
|
|
7637
7980
|
let savingName = "";
|
|
@@ -7640,6 +7983,7 @@ async function openNativeToolsSelector() {
|
|
|
7640
7983
|
renderNativeResourceToggles(tools, {
|
|
7641
7984
|
savingName,
|
|
7642
7985
|
filter,
|
|
7986
|
+
getResourceTag: nativeToolOriginTag,
|
|
7643
7987
|
onToggle: async (tool) => {
|
|
7644
7988
|
if (!tool || savingName) return;
|
|
7645
7989
|
const enabledTools = new Set(tools.filter((item) => item.enabled !== false).map((item) => item.name));
|
|
@@ -7674,7 +8018,7 @@ async function openNativeToolsSelector() {
|
|
|
7674
8018
|
}
|
|
7675
8019
|
|
|
7676
8020
|
async function openNativeSkillsSelector() {
|
|
7677
|
-
openNativeCommandDialog({ title: "
|
|
8021
|
+
openNativeCommandDialog({ title: "Skills Setup", message: "Enable or disable skills for automatic model invocation in the active Pi tab. Disabled skills are removed from the system prompt and their /skill:name commands are blocked by Web UI.", searchPlaceholder: "Filter skills…" });
|
|
7678
8022
|
renderNativeLoading("Loading skills…");
|
|
7679
8023
|
let skills = [];
|
|
7680
8024
|
let savingName = "";
|
|
@@ -7730,6 +8074,15 @@ function openNativeAuthInfo(mode) {
|
|
|
7730
8074
|
async function handleNativeSlashSelectorCommand(message, { usesPromptInput = false } = {}) {
|
|
7731
8075
|
const name = slashCommandName(message);
|
|
7732
8076
|
if (!NATIVE_SELECTOR_COMMANDS.has(name)) return false;
|
|
8077
|
+
const featureId = optionalFeatureIdForCommand(name);
|
|
8078
|
+
if (featureId && !isOptionalFeatureEnabled(featureId)) {
|
|
8079
|
+
const tabContext = activeTabContext();
|
|
8080
|
+
addEvent(commandUnavailableMessage(name), "warn");
|
|
8081
|
+
refreshCommands(tabContext).catch((error) => {
|
|
8082
|
+
if (isCurrentTabContext(tabContext)) addEvent(error.message || String(error), "error");
|
|
8083
|
+
});
|
|
8084
|
+
return true;
|
|
8085
|
+
}
|
|
7733
8086
|
setComposerActionsOpen(false);
|
|
7734
8087
|
hideCommandSuggestions();
|
|
7735
8088
|
if (usesPromptInput) {
|
|
@@ -7943,7 +8296,6 @@ function handleMessageUpdate(event) {
|
|
|
7943
8296
|
scrollChatToBottom();
|
|
7944
8297
|
} else if (update.type === "thinking_delta") {
|
|
7945
8298
|
const delta = thinkingDeltaText(update);
|
|
7946
|
-
currentRunStreamChars += delta.length;
|
|
7947
8299
|
setRunIndicatorActivity("Thinking…", { scroll: false });
|
|
7948
8300
|
const synced = syncStreamingThinkingFromMessage(event);
|
|
7949
8301
|
if (thinkingOutputVisible && delta && (!synced || !streamThinking?.textContent)) {
|
|
@@ -7960,7 +8312,6 @@ function handleMessageUpdate(event) {
|
|
|
7960
8312
|
setRunIndicatorActivity("Finished thinking; waiting for the next output or action…", { scroll: false });
|
|
7961
8313
|
} else if (update.type === "text_delta" || update.type === "text_end") {
|
|
7962
8314
|
const delta = update.type === "text_delta" ? update.delta || "" : "";
|
|
7963
|
-
currentRunStreamChars += delta.length;
|
|
7964
8315
|
const partialText = assistantTextFromMessage(assistantStreamingMessage(event));
|
|
7965
8316
|
if (typeof partialText === "string") streamRawText = partialText;
|
|
7966
8317
|
else if (update.type === "text_end" && typeof update.content === "string") streamRawText = update.content;
|
|
@@ -7992,6 +8343,7 @@ async function refreshState(tabContext = activeTabContext()) {
|
|
|
7992
8343
|
syncActiveTabActivityFromState(currentState);
|
|
7993
8344
|
syncRunIndicatorFromState(currentState);
|
|
7994
8345
|
renderStatus();
|
|
8346
|
+
requestGitFooterWebuiPayload(tabContext);
|
|
7995
8347
|
}
|
|
7996
8348
|
|
|
7997
8349
|
async function refreshStats(tabContext = activeTabContext()) {
|
|
@@ -8018,7 +8370,6 @@ async function refreshWorkspace(tabContext = activeTabContext()) {
|
|
|
8018
8370
|
cwd: health.cwd,
|
|
8019
8371
|
displayCwd: normalizeDisplayPath(health.cwd),
|
|
8020
8372
|
uptimeMs: latestWorkspace?.uptimeMs || 0,
|
|
8021
|
-
git: { isRepo: false },
|
|
8022
8373
|
}
|
|
8023
8374
|
: null;
|
|
8024
8375
|
}
|
|
@@ -9116,12 +9467,18 @@ function handleExtensionUiRequest(request) {
|
|
|
9116
9467
|
addTransientMessage({ role: "extension", title: "extension output", content: message, level });
|
|
9117
9468
|
return;
|
|
9118
9469
|
}
|
|
9119
|
-
case "setStatus":
|
|
9120
|
-
|
|
9121
|
-
|
|
9470
|
+
case "setStatus": {
|
|
9471
|
+
const statusKey = request.statusKey || "extension";
|
|
9472
|
+
if (request.statusText) {
|
|
9473
|
+
statusEntries.set(statusKey, request.statusText);
|
|
9474
|
+
if (statusKey === GIT_FOOTER_WEBUI_STATUS_KEY) cacheGitFooterWebuiPayload(request.statusText, request.tabId);
|
|
9475
|
+
} else {
|
|
9476
|
+
statusEntries.delete(statusKey);
|
|
9477
|
+
}
|
|
9122
9478
|
updateOptionalFeatureAvailability();
|
|
9123
9479
|
renderStatus();
|
|
9124
9480
|
return;
|
|
9481
|
+
}
|
|
9125
9482
|
case "setWidget":
|
|
9126
9483
|
if (Array.isArray(request.widgetLines)) widgets.set(request.widgetKey || request.id, request);
|
|
9127
9484
|
else widgets.delete(request.widgetKey || request.id);
|
|
@@ -9213,7 +9570,7 @@ function showNextDialog() {
|
|
|
9213
9570
|
if (isGuardrailDialog && /^Block$/i.test(optionLabel)) button.classList.add("guardrail-safe-action");
|
|
9214
9571
|
if (isGuardrailDialog && /^Allow/i.test(optionLabel)) button.classList.add("guardrail-allow-action");
|
|
9215
9572
|
if (isReleaseDialog && /^(?:Yes|All eligible packages\b|Publish selected packages \([1-9]\d*\))/.test(optionLabel)) button.classList.add("primary", "release-publish-action");
|
|
9216
|
-
if (isReleaseDialog && /^Publish selected packages
|
|
9573
|
+
if (isReleaseDialog && /^Publish selected packages$/i.test(optionLabel)) button.classList.add("release-publish-disabled-action");
|
|
9217
9574
|
if (isReleaseDialog && /^\[x\]/.test(optionLabel)) button.classList.add("release-target-option", "release-target-selected");
|
|
9218
9575
|
if (isReleaseDialog && /^\[ \]/.test(optionLabel)) button.classList.add("release-target-option");
|
|
9219
9576
|
if (isReleaseDialog && /^(?:No|Cancel)$/i.test(optionLabel)) button.classList.add("release-cancel-action");
|
|
@@ -9324,13 +9681,11 @@ function handleEvent(event) {
|
|
|
9324
9681
|
}
|
|
9325
9682
|
case "pi_process_exit":
|
|
9326
9683
|
addEvent(`pi rpc exited (${event.code ?? event.signal ?? "unknown"})`, "error");
|
|
9327
|
-
currentRunStartedAt = null;
|
|
9328
9684
|
clearRunIndicatorActivity();
|
|
9329
9685
|
refreshTabs().catch((error) => addEvent(error.message, "error"));
|
|
9330
9686
|
break;
|
|
9331
9687
|
case "pi_process_error":
|
|
9332
9688
|
addEvent(event.error || "pi rpc process error", "error");
|
|
9333
|
-
currentRunStartedAt = null;
|
|
9334
9689
|
clearRunIndicatorActivity();
|
|
9335
9690
|
refreshTabs().catch((error) => addEvent(error.message, "error"));
|
|
9336
9691
|
break;
|
|
@@ -9342,9 +9697,6 @@ function handleEvent(event) {
|
|
|
9342
9697
|
scheduleRefreshState();
|
|
9343
9698
|
break;
|
|
9344
9699
|
case "agent_start":
|
|
9345
|
-
currentRunStartedAt = performance.now();
|
|
9346
|
-
currentRunStreamChars = 0;
|
|
9347
|
-
latestTokPerSecond = null;
|
|
9348
9700
|
if (currentState) currentState = { ...currentState, isStreaming: true };
|
|
9349
9701
|
setRunIndicatorActivity("Agent run started; waiting for first output or action…");
|
|
9350
9702
|
addEvent("agent started");
|
|
@@ -9355,7 +9707,6 @@ function handleEvent(event) {
|
|
|
9355
9707
|
case "agent_end":
|
|
9356
9708
|
addEvent("agent finished");
|
|
9357
9709
|
notifyAgentDone(event.tabId || activeTabId, { activity: event.tabActivity, tabTitle: event.tabTitle });
|
|
9358
|
-
currentRunStartedAt = null;
|
|
9359
9710
|
if (currentState) currentState = { ...currentState, isStreaming: false };
|
|
9360
9711
|
clearRunIndicatorActivity();
|
|
9361
9712
|
markTabOutputSeen();
|
|
@@ -9382,11 +9733,6 @@ function handleEvent(event) {
|
|
|
9382
9733
|
handleMessageUpdate(event);
|
|
9383
9734
|
break;
|
|
9384
9735
|
case "message_end":
|
|
9385
|
-
if (event.message?.role === "assistant" && currentRunStartedAt) {
|
|
9386
|
-
const elapsedSeconds = Math.max(0.5, (performance.now() - currentRunStartedAt) / 1000);
|
|
9387
|
-
const outputTokens = Number(event.message?.usage?.output ?? 0) || Math.max(1, Math.round(currentRunStreamChars / 4));
|
|
9388
|
-
latestTokPerSecond = outputTokens / elapsedSeconds;
|
|
9389
|
-
}
|
|
9390
9736
|
if (runIndicatorIsActive()) setRunIndicatorActivity("Assistant message finished; waiting for the next step…", { scroll: false });
|
|
9391
9737
|
scheduleRefreshMessages();
|
|
9392
9738
|
scheduleRefreshState();
|
|
@@ -9515,18 +9861,46 @@ elements.gitWorkflowButton.addEventListener("click", () => {
|
|
|
9515
9861
|
});
|
|
9516
9862
|
const publishMenuContainer = elements.publishButton.parentElement;
|
|
9517
9863
|
elements.publishButton.addEventListener("click", () => {
|
|
9864
|
+
setNativeCommandMenuOpen(false);
|
|
9865
|
+
setPublishMenuOpen(true);
|
|
9866
|
+
});
|
|
9867
|
+
publishMenuContainer?.addEventListener("pointerenter", () => {
|
|
9868
|
+
setNativeCommandMenuOpen(false);
|
|
9518
9869
|
setPublishMenuOpen(true);
|
|
9519
9870
|
});
|
|
9520
|
-
publishMenuContainer?.addEventListener("pointerenter", () => setPublishMenuOpen(true));
|
|
9521
9871
|
publishMenuContainer?.addEventListener("pointerleave", () => setPublishMenuOpen(false));
|
|
9522
|
-
publishMenuContainer?.addEventListener("focusin", () =>
|
|
9872
|
+
publishMenuContainer?.addEventListener("focusin", () => {
|
|
9873
|
+
setNativeCommandMenuOpen(false);
|
|
9874
|
+
setPublishMenuOpen(true);
|
|
9875
|
+
});
|
|
9523
9876
|
publishMenuContainer?.addEventListener("focusout", () => {
|
|
9524
9877
|
setTimeout(() => {
|
|
9525
9878
|
if (!publishMenuContainer?.contains(document.activeElement)) setPublishMenuOpen(false);
|
|
9526
9879
|
}, 0);
|
|
9527
9880
|
});
|
|
9881
|
+
const nativeCommandMenuContainer = elements.nativeCommandMenuButton.parentElement;
|
|
9882
|
+
elements.nativeCommandMenuButton.addEventListener("click", () => {
|
|
9883
|
+
setPublishMenuOpen(false);
|
|
9884
|
+
setNativeCommandMenuOpen(true);
|
|
9885
|
+
});
|
|
9886
|
+
nativeCommandMenuContainer?.addEventListener("pointerenter", () => {
|
|
9887
|
+
setPublishMenuOpen(false);
|
|
9888
|
+
setNativeCommandMenuOpen(true);
|
|
9889
|
+
});
|
|
9890
|
+
nativeCommandMenuContainer?.addEventListener("pointerleave", () => setNativeCommandMenuOpen(false));
|
|
9891
|
+
nativeCommandMenuContainer?.addEventListener("focusin", () => {
|
|
9892
|
+
setPublishMenuOpen(false);
|
|
9893
|
+
setNativeCommandMenuOpen(true);
|
|
9894
|
+
});
|
|
9895
|
+
nativeCommandMenuContainer?.addEventListener("focusout", () => {
|
|
9896
|
+
setTimeout(() => {
|
|
9897
|
+
if (!nativeCommandMenuContainer?.contains(document.activeElement)) setNativeCommandMenuOpen(false);
|
|
9898
|
+
}, 0);
|
|
9899
|
+
});
|
|
9528
9900
|
elements.releaseNpmButton.addEventListener("click", () => runPublishWorkflow("/release-npm"));
|
|
9529
9901
|
elements.releaseAurButton.addEventListener("click", () => runPublishWorkflow("/release-aur"));
|
|
9902
|
+
elements.nativeSkillsButton.addEventListener("click", () => runNativeCommandMenu("/skills"));
|
|
9903
|
+
elements.nativeToolsButton.addEventListener("click", () => runNativeCommandMenu("/tools"));
|
|
9530
9904
|
elements.gitWorkflowCancelButton.addEventListener("click", () => cancelGitWorkflow());
|
|
9531
9905
|
elements.nativeCommandDialog.addEventListener("close", () => {
|
|
9532
9906
|
elements.nativeCommandSearch.oninput = null;
|
|
@@ -9727,6 +10101,9 @@ document.addEventListener("pointerdown", (event) => {
|
|
|
9727
10101
|
if (publishMenuOpen && !event.target?.closest?.(".composer-publish-menu")) {
|
|
9728
10102
|
setPublishMenuOpen(false);
|
|
9729
10103
|
}
|
|
10104
|
+
if (nativeCommandMenuOpen && !event.target?.closest?.(".composer-native-command-menu")) {
|
|
10105
|
+
setNativeCommandMenuOpen(false);
|
|
10106
|
+
}
|
|
9730
10107
|
if (document.body.classList.contains("mobile-tabs-expanded") && !elements.tabBar.contains(event.target) && !elements.terminalTabsToggleButton.contains(event.target)) {
|
|
9731
10108
|
setMobileTabsExpanded(false);
|
|
9732
10109
|
}
|
|
@@ -9813,6 +10190,10 @@ window.addEventListener("keydown", (event) => {
|
|
|
9813
10190
|
setPublishMenuOpen(false);
|
|
9814
10191
|
return;
|
|
9815
10192
|
}
|
|
10193
|
+
if (nativeCommandMenuOpen) {
|
|
10194
|
+
setNativeCommandMenuOpen(false);
|
|
10195
|
+
return;
|
|
10196
|
+
}
|
|
9816
10197
|
if (document.body.classList.contains("composer-actions-open")) {
|
|
9817
10198
|
setComposerActionsOpen(false);
|
|
9818
10199
|
return;
|