@firstpick/pi-package-webui 0.2.3 → 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 +23 -7
- package/bin/pi-webui.mjs +98 -11
- package/package.json +1 -1
- package/public/app.js +12 -4
- package/tests/mobile-static.test.mjs +6 -1
- package/tests/native-parity.test.mjs +5 -1
package/README.md
CHANGED
|
@@ -117,18 +117,34 @@ Environment variables:
|
|
|
117
117
|
## Main features
|
|
118
118
|
|
|
119
119
|
- Multi-tab Pi sessions with isolated processes, working directories, prompt drafts, and activity state.
|
|
120
|
+
- Automatic tab naming from the first prompt, with `--name <name>` still available for an explicit initial tab name.
|
|
120
121
|
- Streaming chat transcript with Markdown, thinking output, tool/bash cards, queue and compaction events, and abort controls.
|
|
121
|
-
- Prompt composer with uploads, drag/drop/paste, inline image support, slash-command autocomplete, and `@` file/path references.
|
|
122
|
+
- Prompt composer with uploads, drag/drop/paste, inline image support, slash-command autocomplete, and `@` file/path references with live suggestions.
|
|
122
123
|
- Browser dialogs for common Pi selectors such as `/model`, `/settings`, `/theme`, `/fork`, `/clone`, `/resume`, `/tree`, `/scoped-models`, `/tools`, and `/skills`.
|
|
123
124
|
- Model, thinking, session, workspace, theme, optional-feature, Codex usage, network, event, and notification controls in the side panel.
|
|
124
|
-
-
|
|
125
|
-
-
|
|
126
|
-
-
|
|
125
|
+
- Side-panel theme picker backed by optional `@firstpick/pi-themes-bundle` themes when loaded.
|
|
126
|
+
- Per-tab cwd changes, a clickable footer cwd picker, saved path fast picks, server-persisted fast picks, and restart-safe restoration of open tabs.
|
|
127
|
+
- Browser support for Pi extension UI prompts, widgets, status updates, browser notifications when a tab needs an extension UI response and an optional side-panel toggle for agent-done notifications.
|
|
128
|
+
- Feedback reactions (`👍`, `👎`, `?`) on final assistant output plus tool/bash action cards, which can ask Pi to create or update a LEARNING.
|
|
127
129
|
- Mobile-friendly layout and PWA install support where the browser allows it.
|
|
128
130
|
|
|
129
|
-
|
|
131
|
+
Useful browser endpoints exposed by the local server include:
|
|
130
132
|
|
|
131
|
-
|
|
133
|
+
- `GET /api/path-suggestions?tab=<tabId>&query=<path>` for `@` file/path references with live suggestions.
|
|
134
|
+
- `POST /api/action-feedback?tab=<tabId>` for feedback on final assistant output and action cards.
|
|
135
|
+
- `POST /api/optional-feature-install` for installing known optional companion packages from the side panel.
|
|
136
|
+
|
|
137
|
+
For local development, run the checkout helper directly, for example:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
./start-webui.sh --dev --cwd /path/to/project
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Optional companion packages
|
|
144
|
+
|
|
145
|
+
A normal Pi/npm install includes the optional companion packages unless optional dependencies are disabled. Startup checks loaded Pi capabilities directly through RPC-visible commands and live widget events, then the side panel shows each optional feature as enabled, disabled, or install-needed. Installing a missing feature is an explicit, warned action; it is localhost-only, limited to known packages, and requires reloading the active Pi tab after installation.
|
|
146
|
+
|
|
147
|
+
When the standalone global `pi-webui` launcher is used, optional companion installs should target the Pi agent npm root instead of the global npm prefix. Override the target explicitly with `PI_WEBUI_OPTIONAL_FEATURE_INSTALL_ROOT=/path/to/package-root` when needed.
|
|
132
148
|
|
|
133
149
|
Optional companions:
|
|
134
150
|
|
|
@@ -157,7 +173,7 @@ This requires `/git-staged-msg` from `@firstpick/pi-prompts-git-pr`. Review the
|
|
|
157
173
|
## Mobile and PWA notes
|
|
158
174
|
|
|
159
175
|
- The mobile composer starts as a compact `Ask Pi…` input and grows as you type.
|
|
160
|
-
- Installable PWA support and notifications
|
|
176
|
+
- Installable PWA support, blocked-tab browser notifications, and optional agent-done notifications require browser service-worker/notification support and usually require `localhost` or HTTPS.
|
|
161
177
|
- Plain `http://<LAN-IP>` can show the app, but some browsers disable PWA install and notifications there.
|
|
162
178
|
|
|
163
179
|
## Network safety
|
package/bin/pi-webui.mjs
CHANGED
|
@@ -17,6 +17,7 @@ const packageRoot = path.resolve(__dirname, "..");
|
|
|
17
17
|
const publicDir = path.join(packageRoot, "public");
|
|
18
18
|
const webuiHelperExtensionPath = path.join(packageRoot, "webui-rpc-helper.mjs");
|
|
19
19
|
const agentDir = process.env.PI_CODING_AGENT_DIR || path.join(homedir(), ".pi", "agent");
|
|
20
|
+
const OPTIONAL_FEATURE_INSTALL_ROOT_ENV = "PI_WEBUI_OPTIONAL_FEATURE_INSTALL_ROOT";
|
|
20
21
|
const packageJson = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
|
|
21
22
|
const nativeParityMatrix = JSON.parse(await readFile(path.join(packageRoot, "WEBUI_TUI_NATIVE_PARITY.json"), "utf8"));
|
|
22
23
|
const webuiDevServer = isTruthyEnv(process.env.PI_WEBUI_DEV) || isSourceCheckout(packageRoot);
|
|
@@ -40,6 +41,7 @@ const ATTACHMENT_UPLOAD_MAX_TOTAL_BYTES = 64 * 1024 * 1024;
|
|
|
40
41
|
const INLINE_IMAGE_MAX_BYTES = 8 * 1024 * 1024;
|
|
41
42
|
const INLINE_IMAGE_TOTAL_MAX_BYTES = 16 * 1024 * 1024;
|
|
42
43
|
const RPC_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
44
|
+
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
43
45
|
const EVENT_HISTORY_LIMIT = 200;
|
|
44
46
|
const EXTENSION_UI_BLOCKING_METHODS = new Set(["select", "confirm", "input", "editor"]);
|
|
45
47
|
const STATUS_RPC_TIMEOUT_MS = 1_800;
|
|
@@ -768,14 +770,51 @@ function runCommand(command, args, { cwd, timeoutMs = 2000, maxOutputLength = 20
|
|
|
768
770
|
});
|
|
769
771
|
}
|
|
770
772
|
|
|
771
|
-
function
|
|
772
|
-
const parts =
|
|
773
|
+
function nodeModulesParentForPackageRoot(root = packageRoot) {
|
|
774
|
+
const parts = root.split(path.sep);
|
|
773
775
|
const nodeModulesIndex = parts.lastIndexOf("node_modules");
|
|
774
776
|
if (nodeModulesIndex >= 0) {
|
|
775
|
-
const
|
|
776
|
-
return
|
|
777
|
+
const parent = parts.slice(0, nodeModulesIndex).join(path.sep);
|
|
778
|
+
return parent || path.parse(root).root;
|
|
777
779
|
}
|
|
778
|
-
return
|
|
780
|
+
return root;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function declaredDependencySpec(pkg, packageName) {
|
|
784
|
+
return firstDefined(
|
|
785
|
+
pkg?.dependencies?.[packageName],
|
|
786
|
+
pkg?.optionalDependencies?.[packageName],
|
|
787
|
+
pkg?.devDependencies?.[packageName],
|
|
788
|
+
pkg?.peerDependencies?.[packageName],
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
async function installRootDeclaresPackage(root, packageName) {
|
|
793
|
+
const pkg = await readJsonFileIfExists(path.join(root, "package.json"));
|
|
794
|
+
return declaredDependencySpec(pkg, packageName) !== undefined;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function configuredAgentNpmRoot() {
|
|
798
|
+
const root = process.env.PI_CODING_AGENT_DIR ? path.resolve(expandUserPath(process.env.PI_CODING_AGENT_DIR)) : agentDir;
|
|
799
|
+
return path.join(root, "npm");
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
async function optionalDependencyInstallRoot() {
|
|
803
|
+
const configuredRoot = process.env[OPTIONAL_FEATURE_INSTALL_ROOT_ENV];
|
|
804
|
+
if (configuredRoot) return path.resolve(expandUserPath(configuredRoot));
|
|
805
|
+
|
|
806
|
+
const installRoot = nodeModulesParentForPackageRoot(packageRoot);
|
|
807
|
+
if (await installRootDeclaresPackage(installRoot, "@firstpick/pi-package-webui")) return installRoot;
|
|
808
|
+
|
|
809
|
+
const agentNpmRoot = configuredAgentNpmRoot();
|
|
810
|
+
if (installRoot !== agentNpmRoot && await installRootDeclaresPackage(agentNpmRoot, "@firstpick/pi-package-webui")) return agentNpmRoot;
|
|
811
|
+
|
|
812
|
+
if (webuiDevServer) return installRoot;
|
|
813
|
+
|
|
814
|
+
throw makeHttpError(
|
|
815
|
+
500,
|
|
816
|
+
`Could not determine a safe optional feature install root. Set ${OPTIONAL_FEATURE_INSTALL_ROOT_ENV} to the Pi package root.`,
|
|
817
|
+
);
|
|
779
818
|
}
|
|
780
819
|
|
|
781
820
|
function formatCommandForDisplay(command, args) {
|
|
@@ -786,7 +825,7 @@ async function installOptionalFeaturePackage(featureId) {
|
|
|
786
825
|
const packageName = OPTIONAL_FEATURE_PACKAGES.get(featureId);
|
|
787
826
|
if (!packageName) throw makeHttpError(400, `Unknown optional feature: ${featureId}`);
|
|
788
827
|
|
|
789
|
-
const installRoot = optionalDependencyInstallRoot();
|
|
828
|
+
const installRoot = await optionalDependencyInstallRoot();
|
|
790
829
|
const npmCommand = process.env.PI_WEBUI_NPM_BIN || "npm";
|
|
791
830
|
const args = ["install", "--prefix", installRoot, packageName];
|
|
792
831
|
const result = await runCommand(npmCommand, args, {
|
|
@@ -1958,7 +1997,7 @@ function commandFromPost(pathname, body) {
|
|
|
1958
1997
|
}
|
|
1959
1998
|
case "/api/thinking": {
|
|
1960
1999
|
const level = String(body.level || "").trim();
|
|
1961
|
-
if (!
|
|
2000
|
+
if (!THINKING_LEVELS.includes(level)) {
|
|
1962
2001
|
throw new Error("Invalid thinking level");
|
|
1963
2002
|
}
|
|
1964
2003
|
return { type: "set_thinking_level", level };
|
|
@@ -2121,10 +2160,21 @@ function rememberTabState(tab, state) {
|
|
|
2121
2160
|
if (!options.noSession && Object.prototype.hasOwnProperty.call(state, "sessionFile")) tab.sessionFile = sessionFileFromState(state);
|
|
2122
2161
|
}
|
|
2123
2162
|
|
|
2163
|
+
function stateWithPendingThinking(tab, state) {
|
|
2164
|
+
if (!state || typeof state !== "object" || !tab?.pendingThinkingLevel) return state;
|
|
2165
|
+
return { ...state, pendingThinkingLevel: tab.pendingThinkingLevel };
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
function responseWithPendingThinking(tab, response) {
|
|
2169
|
+
if (!response || typeof response !== "object" || response.success === false || response.command !== "get_state") return response;
|
|
2170
|
+
return { ...response, data: stateWithPendingThinking(tab, response.data) };
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2124
2173
|
function forgetTabState(tab) {
|
|
2125
2174
|
if (!tab) return;
|
|
2126
2175
|
tab.lastState = null;
|
|
2127
2176
|
tab.sessionFile = undefined;
|
|
2177
|
+
tab.pendingThinkingLevel = undefined;
|
|
2128
2178
|
}
|
|
2129
2179
|
|
|
2130
2180
|
function tabRestorableSessionFile(tab) {
|
|
@@ -2481,6 +2531,7 @@ async function createTab({ id: requestedId, index, title, titleSource, conversat
|
|
|
2481
2531
|
createdAt,
|
|
2482
2532
|
sessionFile: options.noSession ? undefined : normalizedRestoreString(sessionFile, 4096),
|
|
2483
2533
|
lastState: null,
|
|
2534
|
+
pendingThinkingLevel: undefined,
|
|
2484
2535
|
activity: createTabActivity(createdAt),
|
|
2485
2536
|
pendingExtensionUiRequests: new Map(),
|
|
2486
2537
|
webuiHelperRequests: new Map(),
|
|
@@ -2523,6 +2574,7 @@ function tabMeta(tab) {
|
|
|
2523
2574
|
conversationStarted: !!tab.conversationStarted,
|
|
2524
2575
|
cwd: tab.cwd,
|
|
2525
2576
|
sessionFile: tabRestorableSessionFile(tab),
|
|
2577
|
+
pendingThinkingLevel: tab.pendingThinkingLevel || null,
|
|
2526
2578
|
createdAt: tab.createdAt,
|
|
2527
2579
|
startedAt: tab.rpc.startedAt,
|
|
2528
2580
|
pid: tab.rpc.child?.pid,
|
|
@@ -2767,10 +2819,10 @@ function fallbackRpcResponse(tab, command, error) {
|
|
|
2767
2819
|
|
|
2768
2820
|
async function safeRpcResponse(tab, command, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
2769
2821
|
try {
|
|
2770
|
-
return await tab.rpc.send(command, timeoutMs);
|
|
2822
|
+
return responseWithPendingThinking(tab, await tab.rpc.send(command, timeoutMs));
|
|
2771
2823
|
} catch (error) {
|
|
2772
2824
|
const message = sanitizeError(error);
|
|
2773
|
-
if (/Pi RPC process is not running/i.test(message)) return fallbackRpcResponse(tab, command, error);
|
|
2825
|
+
if (/Pi RPC process is not running/i.test(message)) return responseWithPendingThinking(tab, fallbackRpcResponse(tab, command, error));
|
|
2774
2826
|
throw error;
|
|
2775
2827
|
}
|
|
2776
2828
|
}
|
|
@@ -3732,12 +3784,38 @@ async function safeRpcData(tab, command, timeoutMs = STATUS_RPC_TIMEOUT_MS) {
|
|
|
3732
3784
|
const response = await tab.rpc.send(command, timeoutMs);
|
|
3733
3785
|
if (response?.success === false) return { ok: false, error: response.error || `${command.type} failed` };
|
|
3734
3786
|
if (command?.type === "get_state") rememberTabState(tab, response?.data);
|
|
3735
|
-
return { ok: true, data: response?.data ?? null };
|
|
3787
|
+
return { ok: true, data: command?.type === "get_state" ? stateWithPendingThinking(tab, response?.data) : response?.data ?? null };
|
|
3736
3788
|
} catch (error) {
|
|
3737
3789
|
return { ok: false, error: sanitizeError(error) };
|
|
3738
3790
|
}
|
|
3739
3791
|
}
|
|
3740
3792
|
|
|
3793
|
+
function stateIsBusyForSettings(state) {
|
|
3794
|
+
return !!(state?.isStreaming || state?.isCompacting);
|
|
3795
|
+
}
|
|
3796
|
+
|
|
3797
|
+
async function setThinkingLevelForTab(tab, level, { allowPending = true } = {}) {
|
|
3798
|
+
if (!THINKING_LEVELS.includes(level)) throw makeHttpError(400, "Invalid thinking level");
|
|
3799
|
+
const stateResult = allowPending ? await safeRpcData(tab, { type: "get_state" }, STATUS_RPC_TIMEOUT_MS) : { ok: false };
|
|
3800
|
+
if (allowPending && stateResult.ok && stateIsBusyForSettings(stateResult.data)) {
|
|
3801
|
+
tab.pendingThinkingLevel = level;
|
|
3802
|
+
return rpcSuccess("set_thinking_level", { level, pending: true, message: `Thinking level ${level} will apply to the next prompt.` });
|
|
3803
|
+
}
|
|
3804
|
+
const response = await tab.rpc.send({ type: "set_thinking_level", level });
|
|
3805
|
+
if (response.success !== false) tab.pendingThinkingLevel = undefined;
|
|
3806
|
+
return response;
|
|
3807
|
+
}
|
|
3808
|
+
|
|
3809
|
+
async function applyPendingThinkingBeforePrompt(tab) {
|
|
3810
|
+
const level = tab?.pendingThinkingLevel;
|
|
3811
|
+
if (!level) return null;
|
|
3812
|
+
const stateResult = await safeRpcData(tab, { type: "get_state" }, STATUS_RPC_TIMEOUT_MS);
|
|
3813
|
+
if (stateResult.ok && stateIsBusyForSettings(stateResult.data)) return null;
|
|
3814
|
+
const response = await setThinkingLevelForTab(tab, level, { allowPending: false });
|
|
3815
|
+
if (response.success === false) return response;
|
|
3816
|
+
return { ...response, pendingApplied: true };
|
|
3817
|
+
}
|
|
3818
|
+
|
|
3741
3819
|
function providerList(models) {
|
|
3742
3820
|
const providers = new Set();
|
|
3743
3821
|
for (const model of Array.isArray(models) ? models : []) {
|
|
@@ -4132,6 +4210,11 @@ const server = createServer(async (req, res) => {
|
|
|
4132
4210
|
return;
|
|
4133
4211
|
}
|
|
4134
4212
|
const command = commandFromPost(url.pathname, body);
|
|
4213
|
+
const pendingThinkingResponse = await applyPendingThinkingBeforePrompt(tab);
|
|
4214
|
+
if (pendingThinkingResponse?.success === false) {
|
|
4215
|
+
sendJson(res, 400, responseWithTab(pendingThinkingResponse, tab));
|
|
4216
|
+
return;
|
|
4217
|
+
}
|
|
4135
4218
|
const startsVisibleWork = commandStartsVisibleWork(command);
|
|
4136
4219
|
if (startsVisibleWork) {
|
|
4137
4220
|
maybeNameTabForConversation(tab, command);
|
|
@@ -4194,7 +4277,11 @@ const server = createServer(async (req, res) => {
|
|
|
4194
4277
|
maybeNameTabForConversation(tab, command);
|
|
4195
4278
|
markTabWorking(tab);
|
|
4196
4279
|
}
|
|
4197
|
-
const response = command.type === "
|
|
4280
|
+
const response = command.type === "set_thinking_level"
|
|
4281
|
+
? await setThinkingLevelForTab(tab, command.level)
|
|
4282
|
+
: command.type === "bash"
|
|
4283
|
+
? await sendQueuedBashCommand(tab, command)
|
|
4284
|
+
: await tab.rpc.send(command);
|
|
4198
4285
|
if (response.success === false && startsVisibleWork) markTabIdle(tab);
|
|
4199
4286
|
if (response.success !== false && command.type === "new_session") {
|
|
4200
4287
|
tab.conversationStarted = false;
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -3982,10 +3982,15 @@ function renderStatus() {
|
|
|
3982
3982
|
const compacting = state?.isCompacting ? " · compacting" : "";
|
|
3983
3983
|
|
|
3984
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";
|
|
3985
3990
|
const details = {
|
|
3986
3991
|
Status: `${running}${compacting}`,
|
|
3987
3992
|
Model: modelLabel(state?.model),
|
|
3988
|
-
Thinking:
|
|
3993
|
+
Thinking: thinkingDetail,
|
|
3989
3994
|
Session: state?.sessionName || state?.sessionId || "unknown",
|
|
3990
3995
|
File: state?.sessionFile || "in-memory",
|
|
3991
3996
|
Messages: String(state?.messageCount ?? "?"),
|
|
@@ -3996,7 +4001,7 @@ function renderStatus() {
|
|
|
3996
4001
|
elements.stateDetails.append(make("dt", undefined, key), make("dd", undefined, value));
|
|
3997
4002
|
}
|
|
3998
4003
|
|
|
3999
|
-
if (
|
|
4004
|
+
if (shownThinkingLevel) elements.thinkingSelect.value = shownThinkingLevel;
|
|
4000
4005
|
elements.compactButton.disabled = !!state?.isCompacting;
|
|
4001
4006
|
elements.compactButton.textContent = state?.isCompacting ? "Compacting…" : "Compact";
|
|
4002
4007
|
syncModelSelectToState();
|
|
@@ -9650,8 +9655,11 @@ elements.setModelButton.addEventListener("click", async () => {
|
|
|
9650
9655
|
elements.setThinkingButton.addEventListener("click", async () => {
|
|
9651
9656
|
const tabContext = activeTabContext();
|
|
9652
9657
|
try {
|
|
9653
|
-
await api("/api/thinking", { method: "POST", body: { level: elements.thinkingSelect.value }, tabId: tabContext.tabId });
|
|
9654
|
-
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
|
+
}
|
|
9655
9663
|
} catch (error) {
|
|
9656
9664
|
if (isCurrentTabContext(tabContext)) addEvent(error.message, "error");
|
|
9657
9665
|
}
|
|
@@ -682,7 +682,7 @@ assert.match(app, /function triggerNativeDownload\(download\)/, "frontend should
|
|
|
682
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");
|
|
683
683
|
assert.match(server, /case "\/api\/abort-bash":[\s\S]*?type: "abort_bash"/, "server should expose user bash abort");
|
|
684
684
|
assert.match(server, /function sendQueuedBashCommand\(tab, command\)/, "server should serialize user bash through a per-tab FIFO queue");
|
|
685
|
-
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");
|
|
686
686
|
assert.match(app, /function parseUserBashInput\(message\)/, "frontend should parse leading ! and !! bash commands");
|
|
687
687
|
assert.match(app, /let userBashQueuesByTab = new Map\(\)/, "frontend should keep a per-tab user bash queue");
|
|
688
688
|
assert.match(app, /enqueueUserBashCommand\(parsed, \{ usesPromptInput, targetTabId \}\)/, "frontend should queue additional bash commands while one is active");
|
|
@@ -720,6 +720,11 @@ assert.match(server, /const OPTIONAL_FEATURE_PACKAGES = new Map/, "server should
|
|
|
720
720
|
assert.match(server, /\["tuiSkillsCommand", "@firstpick\/pi-extension-setup-skills"\]/, "server should allow installing the TUI skills optional feature");
|
|
721
721
|
assert.match(server, /\["tuiToolsCommand", "@firstpick\/pi-extension-tools"\]/, "server should allow installing the TUI tools optional feature");
|
|
722
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");
|
|
723
728
|
assert.match(server, /url\.pathname === "\/api\/optional-feature-install" && req\.method === "POST"/, "server should expose optional feature install endpoint");
|
|
724
729
|
assert.match(server, /Installing optional Web UI features is only allowed from localhost/, "optional feature install endpoint should be localhost-only");
|
|
725
730
|
assert.match(server, /url\.pathname === "\/api\/themes" && req\.method === "GET"/, "server should expose GET /api/themes");
|
|
@@ -146,6 +146,10 @@ assert.match(app, /api\("\/api\/abort-bash", \{ method: "POST", body: \{\}, tabI
|
|
|
146
146
|
assert.match(server, /async function cycleTabModel\(tab, direction = "forward"\)/, "server should provide scoped\/all model cycling helper");
|
|
147
147
|
assert.match(server, /url\.pathname === "\/api\/model-cycle" && req\.method === "POST"/, "server should expose model-cycle endpoint for shortcuts");
|
|
148
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");
|
|
149
153
|
assert.match(app, /function handleNativeAppShortcut\(event\)/, "frontend should centralize native app shortcut handling");
|
|
150
154
|
assert.match(app, /openNativeModelSelector\(\)/, "Ctrl+L shortcut should open the native model selector");
|
|
151
155
|
assert.match(app, /cycleModelFromShortcut\(event\.shiftKey \? "backward" : "forward"\)/, "Ctrl+P shortcuts should cycle models forward and backward");
|
|
@@ -157,6 +161,6 @@ assert.match(app, /event\.altKey && key === "ArrowUp"[\s\S]*?restoreQueuedMessag
|
|
|
157
161
|
assert.match(app, /let userBashQueuesByTab = new Map\(\)/, "frontend should track per-tab user bash FIFO queues");
|
|
158
162
|
assert.match(app, /enqueueUserBashCommand\(parsed, \{ usesPromptInput, targetTabId \}\)/, "user bash should enqueue while an active or queued bash command exists");
|
|
159
163
|
assert.match(server, /function sendQueuedBashCommand\(tab, command\)/, "server should serialize user bash commands per tab");
|
|
160
|
-
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");
|
|
161
165
|
assert.ok(pkg.files.includes("WEBUI_TUI_NATIVE_PARITY.json"), "published package should include the native parity matrix");
|
|
162
166
|
assert.ok(pkg.files.includes("webui-rpc-helper.mjs"), "published package should include the Web UI RPC helper extension");
|