@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.
@@ -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": "degraded",
13
+ "webStatus": "implemented",
14
14
  "priority": "P1",
15
15
  "sensitive": false,
16
16
  "guards": ["none"],
17
- "currentBehavior": "Browser dialog covers high-frequency runtime settings only.",
18
- "targetBehavior": "Expose all documented settings with runtime/global/project scopes, source visibility, validation, and reload/restart labels."
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 = { ...event, tabId: tab.id, tabTitle: tab.title, tabActivity: tabActivitySnapshot(tab) };
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) tab.pendingThinkingLevel = undefined;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@firstpick/pi-package-webui",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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",