@firstpick/pi-package-webui 0.2.6 → 0.2.8
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/WEBUI_TUI_NATIVE_PARITY.json +3 -3
- package/bin/pi-webui.mjs +219 -2
- package/package.json +1 -1
- package/public/app.js +639 -79
- package/public/index.html +43 -2
- package/public/styles.css +223 -7
- package/tests/mobile-static.test.mjs +38 -4
- package/tests/native-parity.test.mjs +3 -0
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
"category": "native-command",
|
|
11
11
|
"title": "Settings selector/editor",
|
|
12
12
|
"command": { "name": "settings", "description": "Open settings menu" },
|
|
13
|
-
"webStatus": "
|
|
13
|
+
"webStatus": "implemented",
|
|
14
14
|
"priority": "P1",
|
|
15
15
|
"sensitive": false,
|
|
16
16
|
"guards": ["none"],
|
|
17
|
-
"currentBehavior": "Browser dialog
|
|
18
|
-
"targetBehavior": "
|
|
17
|
+
"currentBehavior": "Browser dialog exposes native TUI settings in concise sections with runtime/browser/reload/TUI badges; reload-needed settings can restart the active tab after saving.",
|
|
18
|
+
"targetBehavior": "Keep native TUI setting coverage current; project/global source editing can be added as an advanced enhancement."
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"id": "/model",
|
package/bin/pi-webui.mjs
CHANGED
|
@@ -42,6 +42,27 @@ const INLINE_IMAGE_MAX_BYTES = 8 * 1024 * 1024;
|
|
|
42
42
|
const INLINE_IMAGE_TOTAL_MAX_BYTES = 16 * 1024 * 1024;
|
|
43
43
|
const RPC_IMAGE_MIME_TYPES = new Set(["image/png", "image/jpeg", "image/webp", "image/gif"]);
|
|
44
44
|
const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"];
|
|
45
|
+
const SETTINGS_TRANSPORT_CHOICES = ["sse", "websocket", "websocket-cached", "auto"];
|
|
46
|
+
const SETTINGS_HTTP_IDLE_TIMEOUT_CHOICES = [
|
|
47
|
+
{ label: "30 sec", timeoutMs: 30_000 },
|
|
48
|
+
{ label: "1 min", timeoutMs: 60_000 },
|
|
49
|
+
{ label: "2 min", timeoutMs: 120_000 },
|
|
50
|
+
{ label: "5 min", timeoutMs: 300_000 },
|
|
51
|
+
{ label: "disabled", timeoutMs: 0 },
|
|
52
|
+
];
|
|
53
|
+
const SETTINGS_DOUBLE_ESCAPE_ACTIONS = ["tree", "fork", "none"];
|
|
54
|
+
const SETTINGS_TREE_FILTER_MODES = ["default", "no-tools", "user-only", "labeled-only", "all"];
|
|
55
|
+
const SETTINGS_IMAGE_WIDTH_CELLS = [60, 80, 120];
|
|
56
|
+
const SETTINGS_EDITOR_PADDING_X = [0, 1, 2, 3];
|
|
57
|
+
const SETTINGS_AUTOCOMPLETE_MAX_VISIBLE = [3, 5, 7, 10, 15, 20];
|
|
58
|
+
const SETTINGS_RELOAD_RECOMMENDED_KEYS = new Set(["transport", "httpIdleTimeoutMs", "autoResizeImages", "blockImages", "enableSkillCommands"]);
|
|
59
|
+
const SETTINGS_RELOAD_LABELS = new Map([
|
|
60
|
+
["transport", "Transport"],
|
|
61
|
+
["httpIdleTimeoutMs", "HTTP idle timeout"],
|
|
62
|
+
["autoResizeImages", "Auto-resize images"],
|
|
63
|
+
["blockImages", "Block images"],
|
|
64
|
+
["enableSkillCommands", "Skill commands"],
|
|
65
|
+
]);
|
|
45
66
|
const EVENT_HISTORY_LIMIT = 200;
|
|
46
67
|
const EXTENSION_UI_BLOCKING_METHODS = new Set(["select", "confirm", "input", "editor"]);
|
|
47
68
|
const STATUS_RPC_TIMEOUT_MS = 1_800;
|
|
@@ -2147,6 +2168,22 @@ function responseWithPendingThinking(tab, response) {
|
|
|
2147
2168
|
return { ...response, data: stateWithPendingThinking(tab, response.data) };
|
|
2148
2169
|
}
|
|
2149
2170
|
|
|
2171
|
+
function eventForTabClients(tab, event) {
|
|
2172
|
+
return {
|
|
2173
|
+
...responseWithPendingThinking(tab, event),
|
|
2174
|
+
tabId: tab.id,
|
|
2175
|
+
tabTitle: tab.title,
|
|
2176
|
+
tabActivity: tabActivitySnapshot(tab),
|
|
2177
|
+
};
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
function broadcastPendingThinkingState(tab, state) {
|
|
2181
|
+
broadcastTabEvent(tab, {
|
|
2182
|
+
...eventForTabClients(tab, { type: "response", command: "get_state", success: true, data: stateWithPendingThinking(tab, state) }),
|
|
2183
|
+
pendingExtensionUiRequestCount: pendingExtensionUiRequests(tab).length,
|
|
2184
|
+
});
|
|
2185
|
+
}
|
|
2186
|
+
|
|
2150
2187
|
function forgetTabState(tab) {
|
|
2151
2188
|
if (!tab) return;
|
|
2152
2189
|
tab.lastState = null;
|
|
@@ -2508,7 +2545,7 @@ function attachRpcToTab(tab, rpc) {
|
|
|
2508
2545
|
tab.rpcUnsubscribe = rpc.onEvent((event) => {
|
|
2509
2546
|
if (resolveWebuiHelperResponse(tab, event) || resolveWebuiHelperRpcResponse(tab, event)) return;
|
|
2510
2547
|
updateTabActivityFromEvent(tab, event);
|
|
2511
|
-
let scopedEvent =
|
|
2548
|
+
let scopedEvent = eventForTabClients(tab, event);
|
|
2512
2549
|
if (event?.type === "pi_process_exit" || event?.type === "pi_process_error") {
|
|
2513
2550
|
clearPendingExtensionUiRequests(tab);
|
|
2514
2551
|
clearExtensionStatuses(tab);
|
|
@@ -3087,6 +3124,167 @@ async function setSkillConfigData(tab, body) {
|
|
|
3087
3124
|
return getMergedSkillConfigData(activeTab);
|
|
3088
3125
|
}
|
|
3089
3126
|
|
|
3127
|
+
function settingsManagerForTab(tab) {
|
|
3128
|
+
return SettingsManager.create(tab?.cwd || options.cwd, agentDir);
|
|
3129
|
+
}
|
|
3130
|
+
|
|
3131
|
+
function nativeSettingsPayload(settingsManager = settingsManagerForTab()) {
|
|
3132
|
+
const settings = {
|
|
3133
|
+
transport: settingsManager.getTransport(),
|
|
3134
|
+
httpIdleTimeoutMs: settingsManager.getHttpIdleTimeoutMs(),
|
|
3135
|
+
autoResizeImages: settingsManager.getImageAutoResize(),
|
|
3136
|
+
blockImages: settingsManager.getBlockImages(),
|
|
3137
|
+
enableSkillCommands: settingsManager.getEnableSkillCommands(),
|
|
3138
|
+
hideThinkingBlock: settingsManager.getHideThinkingBlock(),
|
|
3139
|
+
showImages: settingsManager.getShowImages(),
|
|
3140
|
+
imageWidthCells: settingsManager.getImageWidthCells(),
|
|
3141
|
+
collapseChangelog: settingsManager.getCollapseChangelog(),
|
|
3142
|
+
quietStartup: settingsManager.getQuietStartup(),
|
|
3143
|
+
enableInstallTelemetry: settingsManager.getEnableInstallTelemetry(),
|
|
3144
|
+
doubleEscapeAction: settingsManager.getDoubleEscapeAction(),
|
|
3145
|
+
treeFilterMode: settingsManager.getTreeFilterMode(),
|
|
3146
|
+
showHardwareCursor: settingsManager.getShowHardwareCursor(),
|
|
3147
|
+
editorPaddingX: settingsManager.getEditorPaddingX(),
|
|
3148
|
+
autocompleteMaxVisible: settingsManager.getAutocompleteMaxVisible(),
|
|
3149
|
+
clearOnShrink: settingsManager.getClearOnShrink(),
|
|
3150
|
+
showTerminalProgress: settingsManager.getShowTerminalProgress(),
|
|
3151
|
+
warnings: settingsManager.getWarnings(),
|
|
3152
|
+
};
|
|
3153
|
+
return {
|
|
3154
|
+
settings,
|
|
3155
|
+
options: {
|
|
3156
|
+
thinkingLevels: THINKING_LEVELS,
|
|
3157
|
+
transports: SETTINGS_TRANSPORT_CHOICES,
|
|
3158
|
+
httpIdleTimeouts: SETTINGS_HTTP_IDLE_TIMEOUT_CHOICES,
|
|
3159
|
+
doubleEscapeActions: SETTINGS_DOUBLE_ESCAPE_ACTIONS,
|
|
3160
|
+
treeFilterModes: SETTINGS_TREE_FILTER_MODES,
|
|
3161
|
+
imageWidthCells: SETTINGS_IMAGE_WIDTH_CELLS,
|
|
3162
|
+
editorPaddingX: SETTINGS_EDITOR_PADDING_X,
|
|
3163
|
+
autocompleteMaxVisible: SETTINGS_AUTOCOMPLETE_MAX_VISIBLE,
|
|
3164
|
+
},
|
|
3165
|
+
scope: "global",
|
|
3166
|
+
paths: {
|
|
3167
|
+
global: settingsManager.storage?.globalSettingsPath || path.join(agentDir, "settings.json"),
|
|
3168
|
+
project: settingsManager.storage?.projectSettingsPath || path.join(options.cwd, ".pi", "settings.json"),
|
|
3169
|
+
},
|
|
3170
|
+
};
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
function hasOwnSetting(body, key) {
|
|
3174
|
+
return Object.prototype.hasOwnProperty.call(body || {}, key);
|
|
3175
|
+
}
|
|
3176
|
+
|
|
3177
|
+
function requireBooleanSetting(value, key) {
|
|
3178
|
+
if (typeof value !== "boolean") throw makeHttpError(400, `${key} must be a boolean`);
|
|
3179
|
+
return value;
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
function requireStringChoiceSetting(value, key, choices) {
|
|
3183
|
+
const text = String(value ?? "").trim();
|
|
3184
|
+
if (!choices.includes(text)) throw makeHttpError(400, `${key} must be one of: ${choices.join(", ")}`);
|
|
3185
|
+
return text;
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
function requireNumberChoiceSetting(value, key, choices) {
|
|
3189
|
+
const number = Number(value);
|
|
3190
|
+
if (!Number.isFinite(number) || !choices.includes(number)) throw makeHttpError(400, `${key} must be one of: ${choices.join(", ")}`);
|
|
3191
|
+
return number;
|
|
3192
|
+
}
|
|
3193
|
+
|
|
3194
|
+
function rememberSettingChange(changed, reloadRecommended, key, before, after) {
|
|
3195
|
+
if (before === after) return;
|
|
3196
|
+
changed.push(key);
|
|
3197
|
+
if (SETTINGS_RELOAD_RECOMMENDED_KEYS.has(key)) reloadRecommended.push(SETTINGS_RELOAD_LABELS.get(key) || key);
|
|
3198
|
+
}
|
|
3199
|
+
|
|
3200
|
+
function applyBooleanSetting(body, key, settingsManager, getter, setter, changed, reloadRecommended) {
|
|
3201
|
+
if (!hasOwnSetting(body, key)) return;
|
|
3202
|
+
const next = requireBooleanSetting(body[key], key);
|
|
3203
|
+
const before = getter.call(settingsManager);
|
|
3204
|
+
if (before !== next) setter.call(settingsManager, next);
|
|
3205
|
+
rememberSettingChange(changed, reloadRecommended, key, before, next);
|
|
3206
|
+
}
|
|
3207
|
+
|
|
3208
|
+
function applyStringChoiceSetting(body, key, choices, settingsManager, getter, setter, changed, reloadRecommended) {
|
|
3209
|
+
if (!hasOwnSetting(body, key)) return;
|
|
3210
|
+
const next = requireStringChoiceSetting(body[key], key, choices);
|
|
3211
|
+
const before = getter.call(settingsManager);
|
|
3212
|
+
if (before !== next) setter.call(settingsManager, next);
|
|
3213
|
+
rememberSettingChange(changed, reloadRecommended, key, before, next);
|
|
3214
|
+
}
|
|
3215
|
+
|
|
3216
|
+
function applyNumberChoiceSetting(body, key, choices, settingsManager, getter, setter, changed, reloadRecommended) {
|
|
3217
|
+
if (!hasOwnSetting(body, key)) return;
|
|
3218
|
+
const next = requireNumberChoiceSetting(body[key], key, choices);
|
|
3219
|
+
const before = getter.call(settingsManager);
|
|
3220
|
+
if (before !== next) setter.call(settingsManager, next);
|
|
3221
|
+
rememberSettingChange(changed, reloadRecommended, key, before, next);
|
|
3222
|
+
}
|
|
3223
|
+
|
|
3224
|
+
function applyHttpIdleTimeoutSetting(body, settingsManager, changed, reloadRecommended) {
|
|
3225
|
+
const key = "httpIdleTimeoutMs";
|
|
3226
|
+
if (!hasOwnSetting(body, key)) return;
|
|
3227
|
+
const next = Number(body[key]);
|
|
3228
|
+
if (!Number.isFinite(next) || next < 0) throw makeHttpError(400, `${key} must be a non-negative number`);
|
|
3229
|
+
const normalized = Math.floor(next);
|
|
3230
|
+
const before = settingsManager.getHttpIdleTimeoutMs();
|
|
3231
|
+
if (before !== normalized) settingsManager.setHttpIdleTimeoutMs(normalized);
|
|
3232
|
+
rememberSettingChange(changed, reloadRecommended, key, before, normalized);
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
async function setNativeSettingsData(tab, body) {
|
|
3236
|
+
const submitted = body?.settings && typeof body.settings === "object" ? body.settings : {};
|
|
3237
|
+
const settingsManager = settingsManagerForTab(tab);
|
|
3238
|
+
const changed = [];
|
|
3239
|
+
const reloadRecommended = [];
|
|
3240
|
+
|
|
3241
|
+
applyStringChoiceSetting(submitted, "transport", SETTINGS_TRANSPORT_CHOICES, settingsManager, settingsManager.getTransport, settingsManager.setTransport, changed, reloadRecommended);
|
|
3242
|
+
applyHttpIdleTimeoutSetting(submitted, settingsManager, changed, reloadRecommended);
|
|
3243
|
+
applyBooleanSetting(submitted, "autoResizeImages", settingsManager, settingsManager.getImageAutoResize, settingsManager.setImageAutoResize, changed, reloadRecommended);
|
|
3244
|
+
applyBooleanSetting(submitted, "blockImages", settingsManager, settingsManager.getBlockImages, settingsManager.setBlockImages, changed, reloadRecommended);
|
|
3245
|
+
applyBooleanSetting(submitted, "enableSkillCommands", settingsManager, settingsManager.getEnableSkillCommands, settingsManager.setEnableSkillCommands, changed, reloadRecommended);
|
|
3246
|
+
applyBooleanSetting(submitted, "hideThinkingBlock", settingsManager, settingsManager.getHideThinkingBlock, settingsManager.setHideThinkingBlock, changed, reloadRecommended);
|
|
3247
|
+
applyBooleanSetting(submitted, "showImages", settingsManager, settingsManager.getShowImages, settingsManager.setShowImages, changed, reloadRecommended);
|
|
3248
|
+
applyNumberChoiceSetting(submitted, "imageWidthCells", SETTINGS_IMAGE_WIDTH_CELLS, settingsManager, settingsManager.getImageWidthCells, settingsManager.setImageWidthCells, changed, reloadRecommended);
|
|
3249
|
+
applyBooleanSetting(submitted, "collapseChangelog", settingsManager, settingsManager.getCollapseChangelog, settingsManager.setCollapseChangelog, changed, reloadRecommended);
|
|
3250
|
+
applyBooleanSetting(submitted, "quietStartup", settingsManager, settingsManager.getQuietStartup, settingsManager.setQuietStartup, changed, reloadRecommended);
|
|
3251
|
+
applyBooleanSetting(submitted, "enableInstallTelemetry", settingsManager, settingsManager.getEnableInstallTelemetry, settingsManager.setEnableInstallTelemetry, changed, reloadRecommended);
|
|
3252
|
+
applyStringChoiceSetting(submitted, "doubleEscapeAction", SETTINGS_DOUBLE_ESCAPE_ACTIONS, settingsManager, settingsManager.getDoubleEscapeAction, settingsManager.setDoubleEscapeAction, changed, reloadRecommended);
|
|
3253
|
+
applyStringChoiceSetting(submitted, "treeFilterMode", SETTINGS_TREE_FILTER_MODES, settingsManager, settingsManager.getTreeFilterMode, settingsManager.setTreeFilterMode, changed, reloadRecommended);
|
|
3254
|
+
applyBooleanSetting(submitted, "showHardwareCursor", settingsManager, settingsManager.getShowHardwareCursor, settingsManager.setShowHardwareCursor, changed, reloadRecommended);
|
|
3255
|
+
applyNumberChoiceSetting(submitted, "editorPaddingX", SETTINGS_EDITOR_PADDING_X, settingsManager, settingsManager.getEditorPaddingX, settingsManager.setEditorPaddingX, changed, reloadRecommended);
|
|
3256
|
+
applyNumberChoiceSetting(submitted, "autocompleteMaxVisible", SETTINGS_AUTOCOMPLETE_MAX_VISIBLE, settingsManager, settingsManager.getAutocompleteMaxVisible, settingsManager.setAutocompleteMaxVisible, changed, reloadRecommended);
|
|
3257
|
+
applyBooleanSetting(submitted, "clearOnShrink", settingsManager, settingsManager.getClearOnShrink, settingsManager.setClearOnShrink, changed, reloadRecommended);
|
|
3258
|
+
applyBooleanSetting(submitted, "showTerminalProgress", settingsManager, settingsManager.getShowTerminalProgress, settingsManager.setShowTerminalProgress, changed, reloadRecommended);
|
|
3259
|
+
|
|
3260
|
+
if (submitted.warnings && typeof submitted.warnings === "object" && hasOwnSetting(submitted.warnings, "anthropicExtraUsage")) {
|
|
3261
|
+
const warnings = settingsManager.getWarnings();
|
|
3262
|
+
const before = warnings.anthropicExtraUsage ?? true;
|
|
3263
|
+
const next = requireBooleanSetting(submitted.warnings.anthropicExtraUsage, "warnings.anthropicExtraUsage");
|
|
3264
|
+
if (before !== next) {
|
|
3265
|
+
settingsManager.setWarnings({ ...warnings, anthropicExtraUsage: next });
|
|
3266
|
+
rememberSettingChange(changed, reloadRecommended, "warnings.anthropicExtraUsage", before, next);
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
await settingsManager.flush();
|
|
3271
|
+
let activeTab = tab;
|
|
3272
|
+
let reloaded = false;
|
|
3273
|
+
const shouldReload = body?.reload === true && reloadRecommended.length > 0;
|
|
3274
|
+
if (shouldReload) {
|
|
3275
|
+
activeTab = await restartTabRpc(tab, "settings");
|
|
3276
|
+
reloaded = true;
|
|
3277
|
+
}
|
|
3278
|
+
|
|
3279
|
+
return {
|
|
3280
|
+
...nativeSettingsPayload(settingsManagerForTab(activeTab)),
|
|
3281
|
+
changed,
|
|
3282
|
+
reloadRecommended: [...new Set(reloadRecommended)],
|
|
3283
|
+
reloaded,
|
|
3284
|
+
tab: tabMeta(activeTab),
|
|
3285
|
+
};
|
|
3286
|
+
}
|
|
3287
|
+
|
|
3090
3288
|
async function annotateSkillCommandState(tab, commands) {
|
|
3091
3289
|
let disabledSkills = new Set();
|
|
3092
3290
|
try {
|
|
@@ -3817,10 +4015,16 @@ async function setThinkingLevelForTab(tab, level, { allowPending = true } = {})
|
|
|
3817
4015
|
const stateResult = allowPending ? await safeRpcData(tab, { type: "get_state" }, STATUS_RPC_TIMEOUT_MS) : { ok: false };
|
|
3818
4016
|
if (allowPending && stateResult.ok && stateIsBusyForSettings(stateResult.data)) {
|
|
3819
4017
|
tab.pendingThinkingLevel = level;
|
|
4018
|
+
broadcastPendingThinkingState(tab, stateResult.data);
|
|
3820
4019
|
return rpcSuccess("set_thinking_level", { level, pending: true, message: `Thinking level ${level} will apply to the next prompt.` });
|
|
3821
4020
|
}
|
|
3822
4021
|
const response = await tab.rpc.send({ type: "set_thinking_level", level });
|
|
3823
|
-
if (response.success !== false)
|
|
4022
|
+
if (response.success !== false) {
|
|
4023
|
+
tab.pendingThinkingLevel = undefined;
|
|
4024
|
+
const updatedState = await safeRpcData(tab, { type: "get_state" }, STATUS_RPC_TIMEOUT_MS);
|
|
4025
|
+
const effectiveLevel = updatedState.ok ? updatedState.data?.thinkingLevel : level;
|
|
4026
|
+
return { ...response, data: { ...(response.data && typeof response.data === "object" ? response.data : {}), level: effectiveLevel || level, requestedLevel: level } };
|
|
4027
|
+
}
|
|
3824
4028
|
return response;
|
|
3825
4029
|
}
|
|
3826
4030
|
|
|
@@ -4206,6 +4410,19 @@ const server = createServer(async (req, res) => {
|
|
|
4206
4410
|
return;
|
|
4207
4411
|
}
|
|
4208
4412
|
|
|
4413
|
+
if (url.pathname === "/api/settings" && req.method === "GET") {
|
|
4414
|
+
const tab = getRequestedTab(req, url);
|
|
4415
|
+
sendJson(res, 200, { ok: true, data: nativeSettingsPayload(settingsManagerForTab(tab)) });
|
|
4416
|
+
return;
|
|
4417
|
+
}
|
|
4418
|
+
|
|
4419
|
+
if (url.pathname === "/api/settings" && req.method === "POST") {
|
|
4420
|
+
const body = await readJsonBody(req);
|
|
4421
|
+
const tab = getRequestedTab(req, url, body);
|
|
4422
|
+
sendJson(res, 200, { ok: true, data: await setNativeSettingsData(tab, body) });
|
|
4423
|
+
return;
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4209
4426
|
if (url.pathname === "/api/commands" && req.method === "GET") {
|
|
4210
4427
|
const tab = getRequestedTab(req, url);
|
|
4211
4428
|
sendJson(res, 200, { type: "response", command: "get_commands", success: true, data: await getCommandData(tab) });
|
package/package.json
CHANGED