@firstpick/pi-package-webui 0.2.2 → 0.2.4
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 +26 -8
- package/WEBUI_TUI_NATIVE_PARITY.json +26 -0
- package/bin/pi-webui.mjs +402 -14
- package/package.json +10 -3
- package/public/app.js +202 -10
- package/public/index.html +2 -2
- package/public/service-worker.js +1 -1
- package/public/styles.css +25 -0
- package/tests/mobile-static.test.mjs +20 -5
- package/tests/native-parity.test.mjs +19 -1
- package/webui-rpc-helper.mjs +231 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firstpick/pi-package-webui",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Pi Web UI companion package with a local browser UI CLI plus /webui-start and /webui-status commands.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -20,13 +20,17 @@
|
|
|
20
20
|
"../pi-extension-git-footer-status/index.ts",
|
|
21
21
|
"../pi-extension-release-aur/index.ts",
|
|
22
22
|
"../pi-extension-release-npm/index.ts",
|
|
23
|
+
"../pi-extension-setup-skills/index.ts",
|
|
23
24
|
"../pi-extension-stats/index.ts",
|
|
24
25
|
"../pi-extension-todo-progress/index.ts",
|
|
26
|
+
"../pi-extension-tools/index.ts",
|
|
25
27
|
"node_modules/@firstpick/pi-extension-git-footer-status/index.ts",
|
|
26
28
|
"node_modules/@firstpick/pi-extension-release-aur/index.ts",
|
|
27
29
|
"node_modules/@firstpick/pi-extension-release-npm/index.ts",
|
|
30
|
+
"node_modules/@firstpick/pi-extension-setup-skills/index.ts",
|
|
28
31
|
"node_modules/@firstpick/pi-extension-stats/index.ts",
|
|
29
|
-
"node_modules/@firstpick/pi-extension-todo-progress/index.ts"
|
|
32
|
+
"node_modules/@firstpick/pi-extension-todo-progress/index.ts",
|
|
33
|
+
"node_modules/@firstpick/pi-extension-tools/index.ts"
|
|
30
34
|
],
|
|
31
35
|
"skills": [
|
|
32
36
|
"../pi-extension-release-aur/skills",
|
|
@@ -45,7 +49,7 @@
|
|
|
45
49
|
"pi-webui": "./bin/pi-webui.mjs"
|
|
46
50
|
},
|
|
47
51
|
"scripts": {
|
|
48
|
-
"check": "node --check public/app.js && node --check bin/pi-webui.mjs && node tests/mobile-static.test.mjs && node tests/native-parity.test.mjs",
|
|
52
|
+
"check": "node --check public/app.js && node --check bin/pi-webui.mjs && node --check webui-rpc-helper.mjs && node tests/mobile-static.test.mjs && node tests/native-parity.test.mjs",
|
|
49
53
|
"test": "node tests/mobile-static.test.mjs && node tests/native-parity.test.mjs"
|
|
50
54
|
},
|
|
51
55
|
"dependencies": {
|
|
@@ -55,13 +59,16 @@
|
|
|
55
59
|
"@firstpick/pi-extension-git-footer-status": "^0.2.1",
|
|
56
60
|
"@firstpick/pi-extension-release-aur": "^0.1.3",
|
|
57
61
|
"@firstpick/pi-extension-release-npm": "^0.3.3",
|
|
62
|
+
"@firstpick/pi-extension-setup-skills": "^0.1.5",
|
|
58
63
|
"@firstpick/pi-extension-stats": "^0.2.0",
|
|
59
64
|
"@firstpick/pi-extension-todo-progress": "^0.1.7",
|
|
65
|
+
"@firstpick/pi-extension-tools": "^0.1.4",
|
|
60
66
|
"@firstpick/pi-prompts-git-pr": "^0.1.0",
|
|
61
67
|
"@firstpick/pi-themes-bundle": "^0.1.1"
|
|
62
68
|
},
|
|
63
69
|
"files": [
|
|
64
70
|
"index.ts",
|
|
71
|
+
"webui-rpc-helper.mjs",
|
|
65
72
|
"bin",
|
|
66
73
|
"public",
|
|
67
74
|
"images",
|
package/public/app.js
CHANGED
|
@@ -144,6 +144,7 @@ let pathFastPicksLoadPromise = null;
|
|
|
144
144
|
let mobileTabsExpanded = false;
|
|
145
145
|
let openTerminalTabGroupKey = null;
|
|
146
146
|
let availableCommands = [];
|
|
147
|
+
let rawAvailableCommands = [];
|
|
147
148
|
let commandSuggestions = [];
|
|
148
149
|
let pathSuggestions = [];
|
|
149
150
|
let suggestionMode = "none";
|
|
@@ -285,7 +286,9 @@ const optionalFeatureAvailability = {
|
|
|
285
286
|
releaseAur: false,
|
|
286
287
|
statsCommand: false,
|
|
287
288
|
gitFooterStatus: false,
|
|
289
|
+
tuiSkillsCommand: false,
|
|
288
290
|
todoProgressWidget: false,
|
|
291
|
+
tuiToolsCommand: false,
|
|
289
292
|
themeBundle: false,
|
|
290
293
|
};
|
|
291
294
|
const OPTIONAL_FEATURES = [
|
|
@@ -310,6 +313,13 @@ const OPTIONAL_FEATURES = [
|
|
|
310
313
|
capabilityLabel: "/release-aur",
|
|
311
314
|
description: "Publish menu action, setup helpers, skills, and AUR release widgets.",
|
|
312
315
|
},
|
|
316
|
+
{
|
|
317
|
+
id: "tuiSkillsCommand",
|
|
318
|
+
label: "TUI Skills command",
|
|
319
|
+
packageName: "@firstpick/pi-extension-setup-skills",
|
|
320
|
+
capabilityLabel: "RPC /skills from setup-skills extension",
|
|
321
|
+
description: "Terminal-native skill setup command alongside WebUI-native /skills toggles.",
|
|
322
|
+
},
|
|
313
323
|
{
|
|
314
324
|
id: "todoProgressWidget",
|
|
315
325
|
label: "Todo progress widget",
|
|
@@ -317,6 +327,13 @@ const OPTIONAL_FEATURES = [
|
|
|
317
327
|
capabilityLabel: "/todo-progress-status or todo-progress widget event",
|
|
318
328
|
description: "Styled live checklist rendering for assistant todo updates.",
|
|
319
329
|
},
|
|
330
|
+
{
|
|
331
|
+
id: "tuiToolsCommand",
|
|
332
|
+
label: "TUI Tools command",
|
|
333
|
+
packageName: "@firstpick/pi-extension-tools",
|
|
334
|
+
capabilityLabel: "RPC /tools from tools extension",
|
|
335
|
+
description: "Terminal-native active-tool manager alongside WebUI-native /tools toggles.",
|
|
336
|
+
},
|
|
320
337
|
{
|
|
321
338
|
id: "gitFooterStatus",
|
|
322
339
|
label: "Git footer status",
|
|
@@ -348,8 +365,8 @@ const OPTIONAL_COMMAND_FEATURES = new Map([
|
|
|
348
365
|
["git-footer-refresh", "gitFooterStatus"],
|
|
349
366
|
["todo-progress-status", "todoProgressWidget"],
|
|
350
367
|
]);
|
|
351
|
-
const HIDDEN_COMMAND_NAMES = new Set(["webui-tree-navigate"]);
|
|
352
|
-
const NATIVE_SELECTOR_COMMANDS = new Set(["model", "settings", "theme", "fork", "clone", "resume", "tree", "login", "logout", "scoped-models"]);
|
|
368
|
+
const HIDDEN_COMMAND_NAMES = new Set(["webui-tree-navigate", "webui-helper"]);
|
|
369
|
+
const NATIVE_SELECTOR_COMMANDS = new Set(["model", "settings", "theme", "fork", "clone", "resume", "tree", "login", "logout", "scoped-models", "tools", "skills"]);
|
|
353
370
|
const optionalFeatureInstallInProgress = new Set();
|
|
354
371
|
|
|
355
372
|
function createGitWorkflowState() {
|
|
@@ -2398,6 +2415,7 @@ function resetActiveTabUi() {
|
|
|
2398
2415
|
liveToolRuns.clear();
|
|
2399
2416
|
liveToolCards.clear();
|
|
2400
2417
|
availableCommands = [];
|
|
2418
|
+
rawAvailableCommands = [];
|
|
2401
2419
|
resetOptionalFeatureAvailability();
|
|
2402
2420
|
commandSuggestions = [];
|
|
2403
2421
|
pathSuggestions = [];
|
|
@@ -3964,10 +3982,15 @@ function renderStatus() {
|
|
|
3964
3982
|
const compacting = state?.isCompacting ? " · compacting" : "";
|
|
3965
3983
|
|
|
3966
3984
|
elements.stateDetails.replaceChildren();
|
|
3985
|
+
const pendingThinkingLevel = state?.pendingThinkingLevel || null;
|
|
3986
|
+
const shownThinkingLevel = pendingThinkingLevel || state?.thinkingLevel;
|
|
3987
|
+
const thinkingDetail = pendingThinkingLevel && pendingThinkingLevel !== state?.thinkingLevel
|
|
3988
|
+
? `${state?.thinkingLevel || "unknown"} → ${pendingThinkingLevel} next prompt`
|
|
3989
|
+
: state?.thinkingLevel || "unknown";
|
|
3967
3990
|
const details = {
|
|
3968
3991
|
Status: `${running}${compacting}`,
|
|
3969
3992
|
Model: modelLabel(state?.model),
|
|
3970
|
-
Thinking:
|
|
3993
|
+
Thinking: thinkingDetail,
|
|
3971
3994
|
Session: state?.sessionName || state?.sessionId || "unknown",
|
|
3972
3995
|
File: state?.sessionFile || "in-memory",
|
|
3973
3996
|
Messages: String(state?.messageCount ?? "?"),
|
|
@@ -3978,7 +4001,7 @@ function renderStatus() {
|
|
|
3978
4001
|
elements.stateDetails.append(make("dt", undefined, key), make("dd", undefined, value));
|
|
3979
4002
|
}
|
|
3980
4003
|
|
|
3981
|
-
if (
|
|
4004
|
+
if (shownThinkingLevel) elements.thinkingSelect.value = shownThinkingLevel;
|
|
3982
4005
|
elements.compactButton.disabled = !!state?.isCompacting;
|
|
3983
4006
|
elements.compactButton.textContent = state?.isCompacting ? "Compacting…" : "Compact";
|
|
3984
4007
|
syncModelSelectToState();
|
|
@@ -6930,6 +6953,7 @@ function optionalFeatureIdForCommand(name) {
|
|
|
6930
6953
|
|
|
6931
6954
|
function isCommandVisible(command) {
|
|
6932
6955
|
if (HIDDEN_COMMAND_NAMES.has(command.name)) return false;
|
|
6956
|
+
if (command.enabled === false) return false;
|
|
6933
6957
|
const featureId = optionalFeatureIdForCommand(command.name);
|
|
6934
6958
|
return !featureId || isOptionalFeatureEnabled(featureId);
|
|
6935
6959
|
}
|
|
@@ -6942,6 +6966,10 @@ function hasAvailableCommand(name) {
|
|
|
6942
6966
|
return availableCommands.some((command) => command.name === name);
|
|
6943
6967
|
}
|
|
6944
6968
|
|
|
6969
|
+
function hasLoadedRpcCommand(name) {
|
|
6970
|
+
return rawAvailableCommands.some((command) => command.name === name && command.source !== "native");
|
|
6971
|
+
}
|
|
6972
|
+
|
|
6945
6973
|
function optionalFeatureUnavailableMessage(featureId) {
|
|
6946
6974
|
const feature = OPTIONAL_FEATURE_BY_ID.get(featureId);
|
|
6947
6975
|
if (!feature) return "Optional feature unavailable.";
|
|
@@ -6988,7 +7016,9 @@ function updateOptionalFeatureAvailability() {
|
|
|
6988
7016
|
optionalFeatureAvailability.releaseAur = hasAvailableCommand("release-aur");
|
|
6989
7017
|
optionalFeatureAvailability.statsCommand = hasAvailableCommand("stats");
|
|
6990
7018
|
optionalFeatureAvailability.gitFooterStatus = hasAvailableCommand("git-footer-refresh") || optionalFeatureAvailability.gitFooterStatus || statusEntries.has("git-footer");
|
|
7019
|
+
optionalFeatureAvailability.tuiSkillsCommand = hasLoadedRpcCommand("skills");
|
|
6991
7020
|
optionalFeatureAvailability.todoProgressWidget = hasAvailableCommand("todo-progress-status") || optionalFeatureAvailability.todoProgressWidget || widgets.has("todo-progress");
|
|
7021
|
+
optionalFeatureAvailability.tuiToolsCommand = hasLoadedRpcCommand("tools");
|
|
6992
7022
|
optionalFeatureAvailability.themeBundle = availableThemes.length > 0;
|
|
6993
7023
|
renderOptionalFeatureControls();
|
|
6994
7024
|
}
|
|
@@ -7131,7 +7161,7 @@ function runPublishWorkflow(command) {
|
|
|
7131
7161
|
|
|
7132
7162
|
function slashCommandName(message) {
|
|
7133
7163
|
const match = String(message || "").trim().match(/^\/([^\s]+)$/);
|
|
7134
|
-
return match ? match[1] : "";
|
|
7164
|
+
return match ? match[1].toLowerCase() : "";
|
|
7135
7165
|
}
|
|
7136
7166
|
|
|
7137
7167
|
function openNativeCommandDialog({ title, message = "", searchPlaceholder = "" } = {}) {
|
|
@@ -7204,7 +7234,17 @@ function renderNativeSelectorItems(items, { emptyText = "No choices.", onSelect,
|
|
|
7204
7234
|
button.addEventListener("click", () => onSelect?.(item));
|
|
7205
7235
|
const title = make("span", "native-selector-title");
|
|
7206
7236
|
title.append(make("strong", undefined, item.label || item.id || "choice"));
|
|
7207
|
-
if (item.badge)
|
|
7237
|
+
if (item.badge) {
|
|
7238
|
+
const badgeState = String(item.badge).toLowerCase();
|
|
7239
|
+
const badge = make("span", `native-selector-badge${item.badgeClass ? ` ${item.badgeClass}` : ""}`, item.badge);
|
|
7240
|
+
badge.dataset.badgeState = badgeState;
|
|
7241
|
+
if (badgeState === "disabled" || String(item.badgeClass || "").includes("disabled")) {
|
|
7242
|
+
badge.style.borderColor = "rgba(255, 159, 67, 0.62)";
|
|
7243
|
+
badge.style.color = "#ff9f43";
|
|
7244
|
+
badge.style.background = "rgba(255, 159, 67, 0.10)";
|
|
7245
|
+
}
|
|
7246
|
+
title.append(badge);
|
|
7247
|
+
}
|
|
7208
7248
|
const detail = make("span", "native-selector-detail", item.description || "");
|
|
7209
7249
|
const meta = make("span", "native-selector-meta", item.meta || "");
|
|
7210
7250
|
button.append(title);
|
|
@@ -7538,6 +7578,145 @@ function openNativeScopedModelsInfo() {
|
|
|
7538
7578
|
elements.nativeCommandBody.append(make("p", "native-command-note", "Use the footer model chip to choose among scoped models. The full native scoped-models editor is still TUI-only."));
|
|
7539
7579
|
}
|
|
7540
7580
|
|
|
7581
|
+
function nativeResourceSourceLabel(resource) {
|
|
7582
|
+
const info = resource?.sourceInfo || {};
|
|
7583
|
+
return [info.source, info.scope, info.origin].filter(Boolean).join(" · ") || resource?.location || "loaded resource";
|
|
7584
|
+
}
|
|
7585
|
+
|
|
7586
|
+
function nativeResourceCounts(resources) {
|
|
7587
|
+
const disabled = resources.filter((resource) => resource.enabled === false).length;
|
|
7588
|
+
return { total: resources.length, disabled, enabled: resources.length - disabled };
|
|
7589
|
+
}
|
|
7590
|
+
|
|
7591
|
+
function nativeResourceFilterMatches(resource, filter) {
|
|
7592
|
+
if (filter === "enabled") return resource.enabled !== false;
|
|
7593
|
+
if (filter === "disabled") return resource.enabled === false;
|
|
7594
|
+
return true;
|
|
7595
|
+
}
|
|
7596
|
+
|
|
7597
|
+
function renderNativeResourceToggles(resources, { savingName, filter = "all", onToggle } = {}) {
|
|
7598
|
+
const filteredResources = resources.filter((resource) => nativeResourceFilterMatches(resource, filter));
|
|
7599
|
+
const counts = nativeResourceCounts(resources);
|
|
7600
|
+
const items = filteredResources.map((resource) => ({
|
|
7601
|
+
id: resource.name,
|
|
7602
|
+
label: resource.name,
|
|
7603
|
+
description: resource.description || "No description provided.",
|
|
7604
|
+
meta: nativeResourceSourceLabel(resource),
|
|
7605
|
+
badge: resource.enabled === false ? "disabled" : "enabled",
|
|
7606
|
+
badgeClass: resource.enabled === false ? "disabled native-selector-badge-disabled" : "enabled native-selector-badge-enabled",
|
|
7607
|
+
disabled: Boolean(savingName),
|
|
7608
|
+
resource,
|
|
7609
|
+
}));
|
|
7610
|
+
const filterLabel = filter === "enabled" ? "enabled" : filter === "disabled" ? "disabled" : "all";
|
|
7611
|
+
renderNativeSelectorItems(items, {
|
|
7612
|
+
emptyText: `No ${filterLabel} entries match this filter.`,
|
|
7613
|
+
onSelect: (item) => onToggle?.(item.resource),
|
|
7614
|
+
});
|
|
7615
|
+
elements.nativeCommandBody.prepend(make("div", "native-resource-summary muted", `${counts.total} total · ${counts.enabled} enabled · ${counts.disabled} disabled · showing ${filterLabel}`));
|
|
7616
|
+
}
|
|
7617
|
+
|
|
7618
|
+
function renderNativeResourceFilterActions(filter, setFilter, render) {
|
|
7619
|
+
elements.nativeCommandActions.replaceChildren();
|
|
7620
|
+
for (const option of [
|
|
7621
|
+
{ value: "all", label: "All" },
|
|
7622
|
+
{ value: "enabled", label: "Enabled" },
|
|
7623
|
+
{ value: "disabled", label: "Disabled" },
|
|
7624
|
+
]) {
|
|
7625
|
+
addNativeCommandAction(option.label, () => {
|
|
7626
|
+
setFilter(option.value);
|
|
7627
|
+
render();
|
|
7628
|
+
}, filter === option.value ? "primary" : undefined);
|
|
7629
|
+
}
|
|
7630
|
+
addNativeCommandAction("Cancel", closeNativeCommandDialog);
|
|
7631
|
+
}
|
|
7632
|
+
|
|
7633
|
+
async function openNativeToolsSelector() {
|
|
7634
|
+
openNativeCommandDialog({ title: "/tools", 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
|
+
renderNativeLoading("Loading tools…");
|
|
7636
|
+
let tools = [];
|
|
7637
|
+
let savingName = "";
|
|
7638
|
+
let filter = "all";
|
|
7639
|
+
const render = () => {
|
|
7640
|
+
renderNativeResourceToggles(tools, {
|
|
7641
|
+
savingName,
|
|
7642
|
+
filter,
|
|
7643
|
+
onToggle: async (tool) => {
|
|
7644
|
+
if (!tool || savingName) return;
|
|
7645
|
+
const enabledTools = new Set(tools.filter((item) => item.enabled !== false).map((item) => item.name));
|
|
7646
|
+
if (tool.enabled === false) enabledTools.add(tool.name);
|
|
7647
|
+
else enabledTools.delete(tool.name);
|
|
7648
|
+
savingName = tool.name;
|
|
7649
|
+
setNativeCommandError("");
|
|
7650
|
+
render();
|
|
7651
|
+
try {
|
|
7652
|
+
const response = await nativeCommandApi("/api/tools", { method: "POST", body: { enabledTools: [...enabledTools] } });
|
|
7653
|
+
tools = Array.isArray(response.data?.tools) ? response.data.tools : [];
|
|
7654
|
+
addTransientMessage({ role: "native", title: "/tools", content: `Tool ${tool.name} ${enabledTools.has(tool.name) ? "enabled" : "disabled"}.`, level: "info" });
|
|
7655
|
+
} catch (error) {
|
|
7656
|
+
setNativeCommandError(error.message || String(error));
|
|
7657
|
+
} finally {
|
|
7658
|
+
savingName = "";
|
|
7659
|
+
render();
|
|
7660
|
+
}
|
|
7661
|
+
},
|
|
7662
|
+
});
|
|
7663
|
+
renderNativeResourceFilterActions(filter, (value) => { filter = value; }, render);
|
|
7664
|
+
};
|
|
7665
|
+
try {
|
|
7666
|
+
const response = await nativeCommandApi("/api/tools");
|
|
7667
|
+
tools = Array.isArray(response.data?.tools) ? response.data.tools : [];
|
|
7668
|
+
elements.nativeCommandSearch.oninput = render;
|
|
7669
|
+
render();
|
|
7670
|
+
} catch (error) {
|
|
7671
|
+
setNativeCommandError(error.message || String(error));
|
|
7672
|
+
elements.nativeCommandBody.replaceChildren();
|
|
7673
|
+
}
|
|
7674
|
+
}
|
|
7675
|
+
|
|
7676
|
+
async function openNativeSkillsSelector() {
|
|
7677
|
+
openNativeCommandDialog({ title: "/skills", 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
|
+
renderNativeLoading("Loading skills…");
|
|
7679
|
+
let skills = [];
|
|
7680
|
+
let savingName = "";
|
|
7681
|
+
let filter = "all";
|
|
7682
|
+
const render = () => {
|
|
7683
|
+
renderNativeResourceToggles(skills, {
|
|
7684
|
+
savingName,
|
|
7685
|
+
filter,
|
|
7686
|
+
onToggle: async (skill) => {
|
|
7687
|
+
if (!skill || savingName) return;
|
|
7688
|
+
const enabledSkills = new Set(skills.filter((item) => item.enabled !== false).map((item) => item.name));
|
|
7689
|
+
if (skill.enabled === false) enabledSkills.add(skill.name);
|
|
7690
|
+
else enabledSkills.delete(skill.name);
|
|
7691
|
+
savingName = skill.name;
|
|
7692
|
+
setNativeCommandError("");
|
|
7693
|
+
render();
|
|
7694
|
+
try {
|
|
7695
|
+
const response = await nativeCommandApi("/api/skills", { method: "POST", body: { enabledSkills: [...enabledSkills] } });
|
|
7696
|
+
skills = Array.isArray(response.data?.skills) ? response.data.skills : [];
|
|
7697
|
+
addTransientMessage({ role: "native", title: "/skills", content: `Skill ${skill.name} ${enabledSkills.has(skill.name) ? "enabled" : "disabled"}.`, level: "info" });
|
|
7698
|
+
refreshCommands(activeTabContext()).catch((error) => addEvent(error.message || String(error), "error"));
|
|
7699
|
+
} catch (error) {
|
|
7700
|
+
setNativeCommandError(error.message || String(error));
|
|
7701
|
+
} finally {
|
|
7702
|
+
savingName = "";
|
|
7703
|
+
render();
|
|
7704
|
+
}
|
|
7705
|
+
},
|
|
7706
|
+
});
|
|
7707
|
+
renderNativeResourceFilterActions(filter, (value) => { filter = value; }, render);
|
|
7708
|
+
};
|
|
7709
|
+
try {
|
|
7710
|
+
const response = await nativeCommandApi("/api/skills");
|
|
7711
|
+
skills = Array.isArray(response.data?.skills) ? response.data.skills : [];
|
|
7712
|
+
elements.nativeCommandSearch.oninput = render;
|
|
7713
|
+
render();
|
|
7714
|
+
} catch (error) {
|
|
7715
|
+
setNativeCommandError(error.message || String(error));
|
|
7716
|
+
elements.nativeCommandBody.replaceChildren();
|
|
7717
|
+
}
|
|
7718
|
+
}
|
|
7719
|
+
|
|
7541
7720
|
function openNativeAuthInfo(mode) {
|
|
7542
7721
|
const command = mode === "logout" ? "/logout" : "/login";
|
|
7543
7722
|
openNativeCommandDialog({ title: command, message: "Provider credential entry is intentionally not implemented in the browser yet." });
|
|
@@ -7582,6 +7761,12 @@ async function handleNativeSlashSelectorCommand(message, { usesPromptInput = fal
|
|
|
7582
7761
|
case "scoped-models":
|
|
7583
7762
|
openNativeScopedModelsInfo();
|
|
7584
7763
|
return true;
|
|
7764
|
+
case "tools":
|
|
7765
|
+
await openNativeToolsSelector();
|
|
7766
|
+
return true;
|
|
7767
|
+
case "skills":
|
|
7768
|
+
await openNativeSkillsSelector();
|
|
7769
|
+
return true;
|
|
7585
7770
|
case "login":
|
|
7586
7771
|
case "logout":
|
|
7587
7772
|
openNativeAuthInfo(name);
|
|
@@ -7969,7 +8154,7 @@ function syncModelSelectToState() {
|
|
|
7969
8154
|
}
|
|
7970
8155
|
}
|
|
7971
8156
|
|
|
7972
|
-
function normalizeCommands(commands) {
|
|
8157
|
+
function normalizeCommands(commands, { dedupe = true } = {}) {
|
|
7973
8158
|
const seen = new Set();
|
|
7974
8159
|
return (commands || [])
|
|
7975
8160
|
.map((command) => ({
|
|
@@ -7977,9 +8162,12 @@ function normalizeCommands(commands) {
|
|
|
7977
8162
|
description: String(command.description || "").trim(),
|
|
7978
8163
|
source: String(command.source || "command").trim(),
|
|
7979
8164
|
location: String(command.location || "").trim(),
|
|
8165
|
+
enabled: command.enabled !== false,
|
|
7980
8166
|
}))
|
|
7981
8167
|
.filter((command) => {
|
|
7982
|
-
if (!command.name
|
|
8168
|
+
if (!command.name) return false;
|
|
8169
|
+
if (!dedupe) return true;
|
|
8170
|
+
if (seen.has(command.name)) return false;
|
|
7983
8171
|
seen.add(command.name);
|
|
7984
8172
|
return true;
|
|
7985
8173
|
})
|
|
@@ -8358,6 +8546,7 @@ async function refreshCommands(tabContext = activeTabContext()) {
|
|
|
8358
8546
|
if (!tabContext.tabId) return;
|
|
8359
8547
|
const response = await api("/api/commands", { tabId: tabContext.tabId });
|
|
8360
8548
|
if (!isCurrentTabContext(tabContext)) return;
|
|
8549
|
+
rawAvailableCommands = normalizeCommands(response.data?.commands || [], { dedupe: false });
|
|
8361
8550
|
availableCommands = normalizeCommands(response.data?.commands || []);
|
|
8362
8551
|
updateOptionalFeatureAvailability();
|
|
8363
8552
|
renderCommands();
|
|
@@ -9466,8 +9655,11 @@ elements.setModelButton.addEventListener("click", async () => {
|
|
|
9466
9655
|
elements.setThinkingButton.addEventListener("click", async () => {
|
|
9467
9656
|
const tabContext = activeTabContext();
|
|
9468
9657
|
try {
|
|
9469
|
-
await api("/api/thinking", { method: "POST", body: { level: elements.thinkingSelect.value }, tabId: tabContext.tabId });
|
|
9470
|
-
if (isCurrentTabContext(tabContext))
|
|
9658
|
+
const response = await api("/api/thinking", { method: "POST", body: { level: elements.thinkingSelect.value }, tabId: tabContext.tabId });
|
|
9659
|
+
if (isCurrentTabContext(tabContext)) {
|
|
9660
|
+
if (response.data?.pending) addEvent(response.data.message || `Thinking level ${response.data.level} will apply to the next prompt.`, "info");
|
|
9661
|
+
await refreshState(tabContext);
|
|
9662
|
+
}
|
|
9471
9663
|
} catch (error) {
|
|
9472
9664
|
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
9473
9665
|
}
|
package/public/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<link rel="manifest" href="/manifest.webmanifest" />
|
|
13
13
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
14
14
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
|
15
|
-
<link rel="stylesheet" href="/styles.css" />
|
|
15
|
+
<link rel="stylesheet" href="/styles.css?v=20" />
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<button id="sidePanelExpandButton" class="side-panel-expand-button" type="button" aria-controls="sidePanel" aria-expanded="false" aria-label="Expand side panel" title="Expand side panel">
|
|
@@ -364,6 +364,6 @@
|
|
|
364
364
|
</form>
|
|
365
365
|
</dialog>
|
|
366
366
|
|
|
367
|
-
<script type="module" src="/app.js"></script>
|
|
367
|
+
<script type="module" src="/app.js?v=20"></script>
|
|
368
368
|
</body>
|
|
369
369
|
</html>
|
package/public/service-worker.js
CHANGED
package/public/styles.css
CHANGED
|
@@ -3464,6 +3464,31 @@ summary { cursor: pointer; color: var(--warning); }
|
|
|
3464
3464
|
text-transform: uppercase;
|
|
3465
3465
|
letter-spacing: 0.07em;
|
|
3466
3466
|
}
|
|
3467
|
+
.native-command-body:has(.native-resource-summary) .native-selector-badge {
|
|
3468
|
+
border-color: rgba(255, 159, 67, 0.62);
|
|
3469
|
+
color: #ff9f43;
|
|
3470
|
+
background: rgba(255, 159, 67, 0.10);
|
|
3471
|
+
}
|
|
3472
|
+
.native-selector-badge.enabled,
|
|
3473
|
+
.native-selector-badge.native-selector-badge-enabled,
|
|
3474
|
+
.native-selector-badge[data-badge-state="enabled"],
|
|
3475
|
+
.native-command-body:has(.native-resource-summary) .native-selector-badge.enabled,
|
|
3476
|
+
.native-command-body:has(.native-resource-summary) .native-selector-badge.native-selector-badge-enabled,
|
|
3477
|
+
.native-command-body:has(.native-resource-summary) .native-selector-badge[data-badge-state="enabled"] {
|
|
3478
|
+
border-color: rgba(166, 227, 161, 0.32);
|
|
3479
|
+
color: var(--ctp-green);
|
|
3480
|
+
background: transparent;
|
|
3481
|
+
}
|
|
3482
|
+
.native-selector-badge.disabled,
|
|
3483
|
+
.native-selector-badge.native-selector-badge-disabled,
|
|
3484
|
+
.native-selector-badge[data-badge-state="disabled"],
|
|
3485
|
+
.native-command-body:has(.native-resource-summary) .native-selector-badge.disabled,
|
|
3486
|
+
.native-command-body:has(.native-resource-summary) .native-selector-badge.native-selector-badge-disabled,
|
|
3487
|
+
.native-command-body:has(.native-resource-summary) .native-selector-badge[data-badge-state="disabled"] {
|
|
3488
|
+
border-color: rgba(255, 159, 67, 0.62) !important;
|
|
3489
|
+
color: #ff9f43 !important;
|
|
3490
|
+
background: rgba(255, 159, 67, 0.10);
|
|
3491
|
+
}
|
|
3467
3492
|
.native-selector-detail,
|
|
3468
3493
|
.native-selector-meta,
|
|
3469
3494
|
.native-settings-hint {
|
|
@@ -27,8 +27,10 @@ const companionDependencies = {
|
|
|
27
27
|
"@firstpick/pi-extension-git-footer-status": "^0.2.1",
|
|
28
28
|
"@firstpick/pi-extension-release-aur": "^0.1.3",
|
|
29
29
|
"@firstpick/pi-extension-release-npm": "^0.3.3",
|
|
30
|
+
"@firstpick/pi-extension-setup-skills": "^0.1.5",
|
|
30
31
|
"@firstpick/pi-extension-stats": "^0.2.0",
|
|
31
32
|
"@firstpick/pi-extension-todo-progress": "^0.1.7",
|
|
33
|
+
"@firstpick/pi-extension-tools": "^0.1.4",
|
|
32
34
|
"@firstpick/pi-prompts-git-pr": "^0.1.0",
|
|
33
35
|
"@firstpick/pi-themes-bundle": "^0.1.1",
|
|
34
36
|
};
|
|
@@ -340,7 +342,9 @@ assert.match(app, /function setOptionalControlState\(button, available, unavaila
|
|
|
340
342
|
assert.match(app, /function renderCommands\(\)/, "side-panel commands should be re-renderable from current optional feature state");
|
|
341
343
|
assert.match(app, /function installOptionalFeature\(featureId\)/, "optional features should expose an install action");
|
|
342
344
|
assert.match(app, /api\("\/api\/optional-feature-install"/, "optional feature install action should call the backend installer endpoint");
|
|
343
|
-
assert.match(app, /
|
|
345
|
+
assert.match(app, /id: "tuiSkillsCommand"[\s\S]*?@firstpick\/pi-extension-setup-skills/, "optional features should include the TUI skills command companion");
|
|
346
|
+
assert.match(app, /id: "tuiToolsCommand"[\s\S]*?@firstpick\/pi-extension-tools/, "optional features should include the TUI tools command companion");
|
|
347
|
+
assert.match(app, /function updateOptionalFeatureAvailability\(\)[\s\S]*hasAvailableCommand\("git-staged-msg"\)[\s\S]*hasAvailableCommand\("release-npm"\)[\s\S]*hasAvailableCommand\("release-aur"\)[\s\S]*hasLoadedRpcCommand\("skills"\)[\s\S]*hasAvailableCommand\("todo-progress-status"\)[\s\S]*hasLoadedRpcCommand\("tools"\)/, "optional feature detection should call RPC-visible commands directly and distinguish native resource selectors from TUI companions");
|
|
344
348
|
assert.match(app, /if \(!isOptionalFeatureEnabled\("todoProgressWidget"\)\) return String\(text \|\| ""\)/, "todo progress line stripping should only run when the todo feature is detected and enabled");
|
|
345
349
|
assert.match(app, /const releasePrompt = detectedReleasePrompt && isOptionalFeatureEnabled\(detectedReleasePrompt\.featureId\) \? detectedReleasePrompt : null/, "release confirmation dialogs should use specialized rendering only when their release optional feature is enabled");
|
|
346
350
|
assert.match(app, /case "webui_tab_reloaded":[\s\S]*resetOptionalFeatureAvailability\(\)/, "optional feature state should reset when the RPC tab reloads resources");
|
|
@@ -490,7 +494,7 @@ assert.match(app, /async function sendPrompt\(kind = "prompt", explicitMessage\)
|
|
|
490
494
|
assert.match(app, /const rawMessage = usesPromptInput \? elements\.promptInput\.value : explicitMessage/, "direct prompt sends should not read the input textarea");
|
|
491
495
|
assert.match(app, /if \(usesPromptInput\) \{[\s\S]*?if \(targetStillActive\) \{[\s\S]*?elements\.promptInput\.value = "";/, "direct prompt sends should preserve the input textarea draft");
|
|
492
496
|
assert.match(app, /make\("button", "command-item"\)[\s\S]*?sendPrompt\("prompt", `\/\$\{command\.name\}`\)/, "side-panel command clicks should send the slash command directly");
|
|
493
|
-
assert.match(app, /const NATIVE_SELECTOR_COMMANDS = new Set\(\["model", "settings", "theme", "fork", "clone", "resume", "tree", "login", "logout", "scoped-models"\]\)/, "frontend should route native slash commands into selector UIs");
|
|
497
|
+
assert.match(app, /const NATIVE_SELECTOR_COMMANDS = new Set\(\["model", "settings", "theme", "fork", "clone", "resume", "tree", "login", "logout", "scoped-models", "tools", "skills"\]\)/, "frontend should route native slash commands into selector UIs");
|
|
494
498
|
assert.match(app, /async function handleNativeSlashSelectorCommand\(message/, "frontend should intercept exact native slash commands before prompt forwarding");
|
|
495
499
|
assert.match(app, /kind === "prompt" && attachments\.length === 0 && await handleNativeSlashSelectorCommand/, "prompt sending should open native selector dialogs before marking a run active");
|
|
496
500
|
assert.match(app, /function openNativeModelSelector\(\)[\s\S]*?nativeCommandApi\("\/api\/models"\)/, "native /model selector should load models through the active tab API");
|
|
@@ -499,7 +503,7 @@ assert.match(app, /function openNativeForkSelector\(\)[\s\S]*?\/api\/fork-messag
|
|
|
499
503
|
assert.match(app, /function openNativeResumeSelector\(scope = "current"\)[\s\S]*?\/api\/sessions\?scope=\$\{encodeURIComponent\(selectedScope\)\}/, "native /resume selector should list current-cwd or all sessions");
|
|
500
504
|
assert.match(app, /function openNativeTreeSelector\(\)[\s\S]*?\/api\/session-tree[\s\S]*?\/api\/tree-navigate/, "native /tree selector should list tree entries and navigate through the backend helper");
|
|
501
505
|
assert.match(app, /Provider credential entry is intentionally not implemented in the browser yet/, "native /login should remain a safe non-secret guidance dialog");
|
|
502
|
-
assert.match(app, /const HIDDEN_COMMAND_NAMES = new Set\(\["webui-tree-navigate"\]\)/, "internal Web UI helper commands should stay out of command pickers");
|
|
506
|
+
assert.match(app, /const HIDDEN_COMMAND_NAMES = new Set\(\["webui-tree-navigate", "webui-helper"\]\)/, "internal Web UI helper commands should stay out of command pickers");
|
|
503
507
|
assert.match(app, /function shouldSendPromptFromEnter\(event\)/, "prompt keyboard handling should be centralized");
|
|
504
508
|
assert.match(app, /const PROMPT_HISTORY_STORAGE_KEY = "pi-webui-prompt-history"/, "prompt history should be persisted per browser for keyboard recall");
|
|
505
509
|
assert.match(app, /function recallPreviousPromptFromHistory\(\)/, "prompt history should support recalling older prompts from the textarea");
|
|
@@ -596,7 +600,7 @@ assert.equal(manifest.start_url, "/", "PWA manifest should start at the web UI r
|
|
|
596
600
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/apple-touch-icon.png" && icon.sizes === "180x180"), "PWA manifest should include a conventional 180px apple touch icon");
|
|
597
601
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-192.png" && icon.sizes === "192x192"), "PWA manifest should include a 192px icon");
|
|
598
602
|
assert.ok(manifest.icons?.some((icon) => icon.src === "/icon-512.png" && icon.sizes === "512x512"), "PWA manifest should include a 512px icon");
|
|
599
|
-
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-
|
|
603
|
+
assert.match(serviceWorker, /const CACHE_NAME = "pi-webui-pwa-v20"/, "PWA service worker should define an app-shell cache");
|
|
600
604
|
assert.match(serviceWorker, /self\.addEventListener\("notificationclick"/, "PWA service worker should focus Web UI when blocked-tab notifications are clicked");
|
|
601
605
|
assert.match(serviceWorker, /event\.notification\.data\?\.url/, "blocked-tab notifications should carry a URL for service-worker click handling");
|
|
602
606
|
assert.match(serviceWorker, /"\/apple-touch-icon\.png"/, "PWA service worker should cache the apple touch icon");
|
|
@@ -678,7 +682,7 @@ assert.match(app, /function triggerNativeDownload\(download\)/, "frontend should
|
|
|
678
682
|
assert.match(server, /case "\/api\/bash": \{[\s\S]*?type: "bash", command, excludeFromContext: body\.excludeFromContext === true/, "server should expose user bash execution with exclude-from-context support");
|
|
679
683
|
assert.match(server, /case "\/api\/abort-bash":[\s\S]*?type: "abort_bash"/, "server should expose user bash abort");
|
|
680
684
|
assert.match(server, /function sendQueuedBashCommand\(tab, command\)/, "server should serialize user bash through a per-tab FIFO queue");
|
|
681
|
-
assert.match(server, /command\.type === "bash"
|
|
685
|
+
assert.match(server, /command\.type === "bash"[\s\S]*?await sendQueuedBashCommand\(tab, command\)[\s\S]*?: await tab\.rpc\.send\(command\)/, "POST routing should use the bash FIFO queue before RPC send");
|
|
682
686
|
assert.match(app, /function parseUserBashInput\(message\)/, "frontend should parse leading ! and !! bash commands");
|
|
683
687
|
assert.match(app, /let userBashQueuesByTab = new Map\(\)/, "frontend should keep a per-tab user bash queue");
|
|
684
688
|
assert.match(app, /enqueueUserBashCommand\(parsed, \{ usesPromptInput, targetTabId \}\)/, "frontend should queue additional bash commands while one is active");
|
|
@@ -713,7 +717,14 @@ assert.match(server, /type: "set_follow_up_mode"/, "server should expose follow-
|
|
|
713
717
|
assert.match(server, /type: "set_auto_compaction"/, "server should expose auto-compaction changes for native /settings");
|
|
714
718
|
assert.match(server, /@firstpick\/pi-themes-bundle/, "server should discover themes from the optional theme package");
|
|
715
719
|
assert.match(server, /const OPTIONAL_FEATURE_PACKAGES = new Map/, "server should whitelist optional feature packages for install actions");
|
|
720
|
+
assert.match(server, /\["tuiSkillsCommand", "@firstpick\/pi-extension-setup-skills"\]/, "server should allow installing the TUI skills optional feature");
|
|
721
|
+
assert.match(server, /\["tuiToolsCommand", "@firstpick\/pi-extension-tools"\]/, "server should allow installing the TUI tools optional feature");
|
|
716
722
|
assert.match(server, /function installOptionalFeaturePackage\(featureId\)/, "server should provide optional feature package installation helper");
|
|
723
|
+
assert.match(server, /PI_WEBUI_OPTIONAL_FEATURE_INSTALL_ROOT/, "optional feature installs should support an explicit package-manager root override");
|
|
724
|
+
assert.match(server, /function configuredAgentNpmRoot\(\)/, "global Web UI launches should install optional feature packages into Pi's agent npm root, not the npm global prefix");
|
|
725
|
+
assert.match(server, /installRootDeclaresPackage\(.*?@firstpick\/pi-package-webui/s, "optional feature installs should only reuse a node_modules parent that declares the Web UI package dependency");
|
|
726
|
+
assert.match(server, /if \(webuiDevServer\) return installRoot/, "source-checkout Web UI launches should still use the checkout root for optional feature installs");
|
|
727
|
+
assert.match(server, /Could not determine a safe optional feature install root/, "optional feature installs should fail closed when no declared package root can be found");
|
|
717
728
|
assert.match(server, /url\.pathname === "\/api\/optional-feature-install" && req\.method === "POST"/, "server should expose optional feature install endpoint");
|
|
718
729
|
assert.match(server, /Installing optional Web UI features is only allowed from localhost/, "optional feature install endpoint should be localhost-only");
|
|
719
730
|
assert.match(server, /url\.pathname === "\/api\/themes" && req\.method === "GET"/, "server should expose GET /api/themes");
|
|
@@ -761,13 +772,17 @@ for (const extensionPath of [
|
|
|
761
772
|
"../pi-extension-git-footer-status/index.ts",
|
|
762
773
|
"../pi-extension-release-aur/index.ts",
|
|
763
774
|
"../pi-extension-release-npm/index.ts",
|
|
775
|
+
"../pi-extension-setup-skills/index.ts",
|
|
764
776
|
"../pi-extension-stats/index.ts",
|
|
765
777
|
"../pi-extension-todo-progress/index.ts",
|
|
778
|
+
"../pi-extension-tools/index.ts",
|
|
766
779
|
"node_modules/@firstpick/pi-extension-git-footer-status/index.ts",
|
|
767
780
|
"node_modules/@firstpick/pi-extension-release-aur/index.ts",
|
|
768
781
|
"node_modules/@firstpick/pi-extension-release-npm/index.ts",
|
|
782
|
+
"node_modules/@firstpick/pi-extension-setup-skills/index.ts",
|
|
769
783
|
"node_modules/@firstpick/pi-extension-stats/index.ts",
|
|
770
784
|
"node_modules/@firstpick/pi-extension-todo-progress/index.ts",
|
|
785
|
+
"node_modules/@firstpick/pi-extension-tools/index.ts",
|
|
771
786
|
]) {
|
|
772
787
|
assert.ok(pkg.pi?.extensions?.includes(extensionPath), `webui Pi manifest should load ${extensionPath} when present`);
|
|
773
788
|
}
|
|
@@ -56,6 +56,8 @@ const requiredNativeCommands = [
|
|
|
56
56
|
"model",
|
|
57
57
|
"theme",
|
|
58
58
|
"scoped-models",
|
|
59
|
+
"tools",
|
|
60
|
+
"skills",
|
|
59
61
|
"export",
|
|
60
62
|
"import",
|
|
61
63
|
"share",
|
|
@@ -116,6 +118,17 @@ assert.match(server, /function nativeCommandUnavailable\(command, details = \{\}
|
|
|
116
118
|
assert.match(server, /default:\n\s+return nativeCommandUnavailable\(parsed\.name\)/, "unsupported native commands should return structured unavailable cards instead of raw HTTP errors");
|
|
117
119
|
assert.match(server, /url\.pathname === "\/api\/native-parity" && req\.method === "GET"/, "server should expose the native parity matrix for clients/tests");
|
|
118
120
|
assert.match(server, /const NATIVE_DOWNLOAD_TOKEN_TTL_MS = 10 \* 60 \* 1000/, "native downloads should use short-lived tokens");
|
|
121
|
+
assert.match(server, /const WEBUI_HELPER_COMMAND = "webui-helper"/, "server should declare the hidden Web UI RPC helper command");
|
|
122
|
+
assert.match(server, /args\.push\("--extension", webuiHelperExtensionPath\)/, "Web UI tabs should force-load the browser-native RPC helper extension");
|
|
123
|
+
assert.match(server, /url\.pathname === "\/api\/tools" && req\.method === "GET"/, "server should expose GET /api/tools for native /tools");
|
|
124
|
+
assert.match(server, /url\.pathname === "\/api\/tools" && req\.method === "POST"/, "server should expose POST /api/tools for native /tools updates");
|
|
125
|
+
assert.match(server, /url\.pathname === "\/api\/skills" && req\.method === "GET"/, "server should expose GET /api/skills for native /skills");
|
|
126
|
+
assert.match(server, /url\.pathname === "\/api\/skills" && req\.method === "POST"/, "server should expose POST /api/skills for native /skills updates");
|
|
127
|
+
assert.match(app, /const HIDDEN_COMMAND_NAMES = new Set\(\["webui-tree-navigate", "webui-helper"\]\)/, "frontend should hide Web UI internal helper commands");
|
|
128
|
+
assert.match(app, /"scoped-models", "tools", "skills"/, "frontend native selector commands should include /tools and /skills");
|
|
129
|
+
assert.match(app, /return match \? match\[1\]\.toLowerCase\(\) : ""/, "frontend native slash command matching should be case-insensitive");
|
|
130
|
+
assert.match(app, /async function openNativeToolsSelector\(\)/, "frontend should implement a browser-native /tools selector");
|
|
131
|
+
assert.match(app, /async function openNativeSkillsSelector\(\)/, "frontend should implement a browser-native /skills selector");
|
|
119
132
|
assert.match(server, /function registerNativeDownload\(filePath, \{ fileName, contentType, command = "native" \} = \{\}\)/, "server should register opaque native download tokens");
|
|
120
133
|
assert.match(server, /url\.pathname\.startsWith\("\/api\/native-download\/"\) && req\.method === "GET"/, "server should expose opaque native download endpoint");
|
|
121
134
|
assert.match(server, /case "export": \{\n\s+return handleNativeExportCommand\(tab, parsed\.args, req\);\n\s+\}/, "native /export should route through the native command adapter");
|
|
@@ -133,6 +146,10 @@ assert.match(app, /api\("\/api\/abort-bash", \{ method: "POST", body: \{\}, tabI
|
|
|
133
146
|
assert.match(server, /async function cycleTabModel\(tab, direction = "forward"\)/, "server should provide scoped\/all model cycling helper");
|
|
134
147
|
assert.match(server, /url\.pathname === "\/api\/model-cycle" && req\.method === "POST"/, "server should expose model-cycle endpoint for shortcuts");
|
|
135
148
|
assert.match(server, /case "\/api\/thinking-cycle":[\s\S]*?type: "cycle_thinking_level"/, "server should expose thinking-cycle endpoint for shortcuts");
|
|
149
|
+
assert.match(server, /async function setThinkingLevelForTab\(tab, level, \{ allowPending = true \} = \{\}\)[\s\S]*?stateIsBusyForSettings\(stateResult\.data\)[\s\S]*?tab\.pendingThinkingLevel = level/, "server should queue side-panel thinking changes while a tab is running");
|
|
150
|
+
assert.match(server, /const pendingThinkingResponse = await applyPendingThinkingBeforePrompt\(tab\)/, "server should apply queued thinking level before the next prompt");
|
|
151
|
+
assert.match(app, /pendingThinkingLevel[\s\S]*?next prompt/, "frontend should show queued thinking changes as applying on the next prompt");
|
|
152
|
+
assert.match(app, /response\.data\?\.pending[\s\S]*?will apply to the next prompt/, "frontend should announce queued side-panel thinking changes");
|
|
136
153
|
assert.match(app, /function handleNativeAppShortcut\(event\)/, "frontend should centralize native app shortcut handling");
|
|
137
154
|
assert.match(app, /openNativeModelSelector\(\)/, "Ctrl+L shortcut should open the native model selector");
|
|
138
155
|
assert.match(app, /cycleModelFromShortcut\(event\.shiftKey \? "backward" : "forward"\)/, "Ctrl+P shortcuts should cycle models forward and backward");
|
|
@@ -144,5 +161,6 @@ assert.match(app, /event\.altKey && key === "ArrowUp"[\s\S]*?restoreQueuedMessag
|
|
|
144
161
|
assert.match(app, /let userBashQueuesByTab = new Map\(\)/, "frontend should track per-tab user bash FIFO queues");
|
|
145
162
|
assert.match(app, /enqueueUserBashCommand\(parsed, \{ usesPromptInput, targetTabId \}\)/, "user bash should enqueue while an active or queued bash command exists");
|
|
146
163
|
assert.match(server, /function sendQueuedBashCommand\(tab, command\)/, "server should serialize user bash commands per tab");
|
|
147
|
-
assert.match(server, /command\.type === "bash"
|
|
164
|
+
assert.match(server, /command\.type === "bash"[\s\S]*?await sendQueuedBashCommand\(tab, command\)[\s\S]*?: await tab\.rpc\.send\(command\)/, "generic POST handling should route bash through the FIFO queue");
|
|
148
165
|
assert.ok(pkg.files.includes("WEBUI_TUI_NATIVE_PARITY.json"), "published package should include the native parity matrix");
|
|
166
|
+
assert.ok(pkg.files.includes("webui-rpc-helper.mjs"), "published package should include the Web UI RPC helper extension");
|