@firstpick/pi-package-webui 0.2.6 → 0.2.7
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/bin/pi-webui.mjs +24 -2
- package/package.json +1 -1
- package/public/app.js +26 -5
- package/tests/mobile-static.test.mjs +4 -0
- package/tests/native-parity.test.mjs +3 -0
package/bin/pi-webui.mjs
CHANGED
|
@@ -2147,6 +2147,22 @@ function responseWithPendingThinking(tab, response) {
|
|
|
2147
2147
|
return { ...response, data: stateWithPendingThinking(tab, response.data) };
|
|
2148
2148
|
}
|
|
2149
2149
|
|
|
2150
|
+
function eventForTabClients(tab, event) {
|
|
2151
|
+
return {
|
|
2152
|
+
...responseWithPendingThinking(tab, event),
|
|
2153
|
+
tabId: tab.id,
|
|
2154
|
+
tabTitle: tab.title,
|
|
2155
|
+
tabActivity: tabActivitySnapshot(tab),
|
|
2156
|
+
};
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
function broadcastPendingThinkingState(tab, state) {
|
|
2160
|
+
broadcastTabEvent(tab, {
|
|
2161
|
+
...eventForTabClients(tab, { type: "response", command: "get_state", success: true, data: stateWithPendingThinking(tab, state) }),
|
|
2162
|
+
pendingExtensionUiRequestCount: pendingExtensionUiRequests(tab).length,
|
|
2163
|
+
});
|
|
2164
|
+
}
|
|
2165
|
+
|
|
2150
2166
|
function forgetTabState(tab) {
|
|
2151
2167
|
if (!tab) return;
|
|
2152
2168
|
tab.lastState = null;
|
|
@@ -2508,7 +2524,7 @@ function attachRpcToTab(tab, rpc) {
|
|
|
2508
2524
|
tab.rpcUnsubscribe = rpc.onEvent((event) => {
|
|
2509
2525
|
if (resolveWebuiHelperResponse(tab, event) || resolveWebuiHelperRpcResponse(tab, event)) return;
|
|
2510
2526
|
updateTabActivityFromEvent(tab, event);
|
|
2511
|
-
let scopedEvent =
|
|
2527
|
+
let scopedEvent = eventForTabClients(tab, event);
|
|
2512
2528
|
if (event?.type === "pi_process_exit" || event?.type === "pi_process_error") {
|
|
2513
2529
|
clearPendingExtensionUiRequests(tab);
|
|
2514
2530
|
clearExtensionStatuses(tab);
|
|
@@ -3817,10 +3833,16 @@ async function setThinkingLevelForTab(tab, level, { allowPending = true } = {})
|
|
|
3817
3833
|
const stateResult = allowPending ? await safeRpcData(tab, { type: "get_state" }, STATUS_RPC_TIMEOUT_MS) : { ok: false };
|
|
3818
3834
|
if (allowPending && stateResult.ok && stateIsBusyForSettings(stateResult.data)) {
|
|
3819
3835
|
tab.pendingThinkingLevel = level;
|
|
3836
|
+
broadcastPendingThinkingState(tab, stateResult.data);
|
|
3820
3837
|
return rpcSuccess("set_thinking_level", { level, pending: true, message: `Thinking level ${level} will apply to the next prompt.` });
|
|
3821
3838
|
}
|
|
3822
3839
|
const response = await tab.rpc.send({ type: "set_thinking_level", level });
|
|
3823
|
-
if (response.success !== false)
|
|
3840
|
+
if (response.success !== false) {
|
|
3841
|
+
tab.pendingThinkingLevel = undefined;
|
|
3842
|
+
const updatedState = await safeRpcData(tab, { type: "get_state" }, STATUS_RPC_TIMEOUT_MS);
|
|
3843
|
+
const effectiveLevel = updatedState.ok ? updatedState.data?.thinkingLevel : level;
|
|
3844
|
+
return { ...response, data: { ...(response.data && typeof response.data === "object" ? response.data : {}), level: effectiveLevel || level, requestedLevel: level } };
|
|
3845
|
+
}
|
|
3824
3846
|
return response;
|
|
3825
3847
|
}
|
|
3826
3848
|
|
package/package.json
CHANGED
package/public/app.js
CHANGED
|
@@ -7288,10 +7288,10 @@ function resetOptionalFeatureAvailability() {
|
|
|
7288
7288
|
renderOptionalFeatureControls();
|
|
7289
7289
|
}
|
|
7290
7290
|
|
|
7291
|
-
function requestGitFooterWebuiPayload(tabContext = activeTabContext()) {
|
|
7291
|
+
function requestGitFooterWebuiPayload(tabContext = activeTabContext(), { force = false } = {}) {
|
|
7292
7292
|
if (!tabContext.tabId || isOptionalFeatureDisabled("gitFooterStatus")) return;
|
|
7293
7293
|
if (currentState?.isStreaming || currentState?.isCompacting) return;
|
|
7294
|
-
if (!hasAvailableCommand("git-footer-refresh") || statusEntries.has(GIT_FOOTER_WEBUI_STATUS_KEY)) return;
|
|
7294
|
+
if (!hasAvailableCommand("git-footer-refresh") || (!force && statusEntries.has(GIT_FOOTER_WEBUI_STATUS_KEY))) return;
|
|
7295
7295
|
if (gitFooterPayloadRefreshInFlightByTab.has(tabContext.tabId)) return;
|
|
7296
7296
|
|
|
7297
7297
|
gitFooterPayloadRefreshInFlightByTab.add(tabContext.tabId);
|
|
@@ -7730,7 +7730,8 @@ function openNativeSettingsDialog() {
|
|
|
7730
7730
|
setNativeCommandError("");
|
|
7731
7731
|
try {
|
|
7732
7732
|
const requests = [];
|
|
7733
|
-
|
|
7733
|
+
const thinkingLevelChanged = thinking.select.value !== state.thinkingLevel;
|
|
7734
|
+
if (thinkingLevelChanged) requests.push(nativeCommandApi("/api/thinking", { method: "POST", body: { level: thinking.select.value } }));
|
|
7734
7735
|
if (steering.select.value !== state.steeringMode) requests.push(nativeCommandApi("/api/steering-mode", { method: "POST", body: { mode: steering.select.value } }));
|
|
7735
7736
|
if (followUp.select.value !== state.followUpMode) requests.push(nativeCommandApi("/api/follow-up-mode", { method: "POST", body: { mode: followUp.select.value } }));
|
|
7736
7737
|
if (autoCompact.input.checked !== state.autoCompactionEnabled) requests.push(nativeCommandApi("/api/auto-compaction", { method: "POST", body: { enabled: autoCompact.input.checked } }));
|
|
@@ -7738,6 +7739,7 @@ function openNativeSettingsDialog() {
|
|
|
7738
7739
|
if (thinkingOutput.input.checked !== thinkingOutputVisible) setThinkingOutputVisible(thinkingOutput.input.checked);
|
|
7739
7740
|
if (doneNotifications.input.checked !== agentDoneNotificationsEnabled) await setAgentDoneNotificationsEnabled(doneNotifications.input.checked);
|
|
7740
7741
|
await Promise.all(requests);
|
|
7742
|
+
if (thinkingLevelChanged) requestGitFooterWebuiPayload(activeTabContext(), { force: true });
|
|
7741
7743
|
addTransientMessage({ role: "native", title: "/settings", content: "Settings updated.", level: "info" });
|
|
7742
7744
|
closeNativeCommandDialog();
|
|
7743
7745
|
await refreshState();
|
|
@@ -8335,15 +8337,26 @@ function handleMessageUpdate(event) {
|
|
|
8335
8337
|
}
|
|
8336
8338
|
}
|
|
8337
8339
|
|
|
8340
|
+
function modelStateKey(model) {
|
|
8341
|
+
return model ? `${model.provider || ""}/${model.id || ""}` : "";
|
|
8342
|
+
}
|
|
8343
|
+
|
|
8344
|
+
function gitFooterRelevantStateChanged(previousState, nextState) {
|
|
8345
|
+
if (!previousState || !nextState) return false;
|
|
8346
|
+
return previousState.thinkingLevel !== nextState.thinkingLevel || modelStateKey(previousState.model) !== modelStateKey(nextState.model);
|
|
8347
|
+
}
|
|
8348
|
+
|
|
8338
8349
|
async function refreshState(tabContext = activeTabContext()) {
|
|
8339
8350
|
if (!tabContext.tabId) return;
|
|
8340
8351
|
const response = await api("/api/state", { tabId: tabContext.tabId });
|
|
8341
8352
|
if (!isCurrentTabContext(tabContext)) return;
|
|
8353
|
+
const previousState = currentState;
|
|
8342
8354
|
currentState = response.data || null;
|
|
8355
|
+
const shouldRefreshGitFooter = gitFooterRelevantStateChanged(previousState, currentState);
|
|
8343
8356
|
syncActiveTabActivityFromState(currentState);
|
|
8344
8357
|
syncRunIndicatorFromState(currentState);
|
|
8345
8358
|
renderStatus();
|
|
8346
|
-
requestGitFooterWebuiPayload(tabContext);
|
|
8359
|
+
requestGitFooterWebuiPayload(tabContext, { force: shouldRefreshGitFooter });
|
|
8347
8360
|
}
|
|
8348
8361
|
|
|
8349
8362
|
async function refreshStats(tabContext = activeTabContext()) {
|
|
@@ -9181,6 +9194,7 @@ async function cycleThinkingFromShortcut() {
|
|
|
9181
9194
|
if (response.data?.level && currentState) currentState = { ...currentState, thinkingLevel: response.data.level };
|
|
9182
9195
|
if (isCurrentTabContext(tabContext)) {
|
|
9183
9196
|
addTransientMessage({ role: "native", title: "thinking", content: response.data?.level ? `Thinking level: ${response.data.level}` : "Thinking level did not change.", level: "info" });
|
|
9197
|
+
if (response.data?.level) requestGitFooterWebuiPayload(tabContext, { force: true });
|
|
9184
9198
|
await Promise.allSettled([refreshState(tabContext), refreshStats(tabContext)]);
|
|
9185
9199
|
}
|
|
9186
9200
|
} catch (error) {
|
|
@@ -10031,7 +10045,14 @@ elements.setThinkingButton.addEventListener("click", async () => {
|
|
|
10031
10045
|
try {
|
|
10032
10046
|
const response = await api("/api/thinking", { method: "POST", body: { level: elements.thinkingSelect.value }, tabId: tabContext.tabId });
|
|
10033
10047
|
if (isCurrentTabContext(tabContext)) {
|
|
10034
|
-
if (response.data?.pending)
|
|
10048
|
+
if (response.data?.pending) {
|
|
10049
|
+
addEvent(response.data.message || `Thinking level ${response.data.level} will apply to the next prompt.`, "info");
|
|
10050
|
+
} else if (response.data?.level) {
|
|
10051
|
+
const requested = response.data.requestedLevel;
|
|
10052
|
+
const effective = response.data.level;
|
|
10053
|
+
addEvent(requested && requested !== effective ? `Thinking level set to ${effective} (requested ${requested}).` : `Thinking level set to ${effective}.`, "info");
|
|
10054
|
+
requestGitFooterWebuiPayload(tabContext, { force: true });
|
|
10055
|
+
}
|
|
10035
10056
|
await refreshState(tabContext);
|
|
10036
10057
|
}
|
|
10037
10058
|
} catch (error) {
|
|
@@ -368,6 +368,10 @@ assert.doesNotMatch(app, /Git footer status disabled/, "disabled git footer shou
|
|
|
368
368
|
assert.doesNotMatch(app, /footerMeta\("runtime"/, "minimal Web UI footer should not render runtime metadata");
|
|
369
369
|
assert.match(app, /statusEntries\.has\(GIT_FOOTER_WEBUI_STATUS_KEY\)/, "optional feature detection should recognize the git-footer-status Web UI payload");
|
|
370
370
|
assert.match(app, /\/git-footer-refresh --webui-silent/, "Web UI should quietly request the extension-owned footer payload when idle and missing");
|
|
371
|
+
assert.match(app, /function requestGitFooterWebuiPayload\(tabContext = activeTabContext\(\), \{ force = false \} = \{\}\)[\s\S]*?!force && statusEntries\.has\(GIT_FOOTER_WEBUI_STATUS_KEY\)/, "git footer payload refresh should support forced refresh even when a live payload already exists");
|
|
372
|
+
assert.match(app, /function gitFooterRelevantStateChanged\(previousState, nextState\)[\s\S]*?previousState\.thinkingLevel !== nextState\.thinkingLevel[\s\S]*?modelStateKey\(previousState\.model\) !== modelStateKey\(nextState\.model\)/, "state refresh should detect model and thinking changes that make the git footer payload stale");
|
|
373
|
+
assert.match(app, /requestGitFooterWebuiPayload\(tabContext, \{ force: shouldRefreshGitFooter \}\)/, "state refresh should force-refresh the git footer when model or thinking state changes");
|
|
374
|
+
assert.match(app, /if \(response\.data\?\.level\) requestGitFooterWebuiPayload\(tabContext, \{ force: true \}\)/, "thinking shortcut should immediately force-refresh the git footer payload");
|
|
371
375
|
assert.match(app, /Loading git footer status…/, "missing git footer payload should show a loading state before declaring the extension unavailable");
|
|
372
376
|
assert.match(app, /GIT_FOOTER_WEBUI_PAYLOAD_CACHE_KEY/, "git footer payloads should be cached across Web UI reloads");
|
|
373
377
|
assert.match(app, /function setOptionalFeatureDisabled\(featureId, disabled\)[\s\S]*clearGitFooterWebuiPayloadCache\(\)/, "changing the git footer feature toggle should invalidate the cached footer payload");
|
|
@@ -147,9 +147,12 @@ assert.match(server, /async function cycleTabModel\(tab, direction = "forward"\)
|
|
|
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
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, /function eventForTabClients\(tab, event\)[\s\S]*?responseWithPendingThinking\(tab, event\)[\s\S]*?tabId: tab\.id/, "server should decorate SSE state responses with pending thinking before broadcasting to clients");
|
|
151
|
+
assert.match(server, /tab\.pendingThinkingLevel = level;\n\s+broadcastPendingThinkingState\(tab, stateResult\.data\)/, "server should broadcast queued thinking state after assigning the pending level");
|
|
150
152
|
assert.match(server, /const pendingThinkingResponse = await applyPendingThinkingBeforePrompt\(tab\)/, "server should apply queued thinking level before the next prompt");
|
|
151
153
|
assert.match(app, /pendingThinkingLevel[\s\S]*?next prompt/, "frontend should show queued thinking changes as applying on the next prompt");
|
|
152
154
|
assert.match(app, /response\.data\?\.pending[\s\S]*?will apply to the next prompt/, "frontend should announce queued side-panel thinking changes");
|
|
155
|
+
assert.match(app, /response\.data\?\.level[\s\S]*?Thinking level set to/, "frontend should announce effective side-panel thinking changes");
|
|
153
156
|
assert.match(app, /function handleNativeAppShortcut\(event\)/, "frontend should centralize native app shortcut handling");
|
|
154
157
|
assert.match(app, /openNativeModelSelector\(\)/, "Ctrl+L shortcut should open the native model selector");
|
|
155
158
|
assert.match(app, /cycleModelFromShortcut\(event\.shiftKey \? "backward" : "forward"\)/, "Ctrl+P shortcuts should cycle models forward and backward");
|