@firstpick/pi-package-webui 0.1.4 → 0.1.5
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 +25 -6
- package/bin/pi-webui.mjs +183 -13
- package/package.json +34 -4
- package/public/app.js +567 -42
- package/public/index.html +3 -0
- package/public/service-worker.js +1 -1
- package/public/styles.css +145 -2
- package/tests/mobile-static.test.mjs +89 -8
package/README.md
CHANGED
|
@@ -17,6 +17,22 @@ This package provides:
|
|
|
17
17
|
- Pi available through this package dependency or as `pi` on `PATH`
|
|
18
18
|
- A modern browser with Server-Sent Events support
|
|
19
19
|
|
|
20
|
+
## Optional companion packages
|
|
21
|
+
|
|
22
|
+
The Web UI declares its companion Pi packages as npm `optionalDependencies`. A normal npm/Pi install will install them, while minimal installs can skip them with npm's optional-dependency controls such as `npm install --omit=optional`.
|
|
23
|
+
|
|
24
|
+
At startup, the browser checks loaded Pi capabilities directly through RPC-visible commands and live widget events; it does not inspect npm package folders. That means locally symlinked/dev packages and separately installed Pi packages work as long as their commands/widgets are loaded in the active Pi tab.
|
|
25
|
+
|
|
26
|
+
The side panel shows each optional feature as enabled, disabled, or install-needed. Disabling a feature is Web UI-local and hides Web UI affordances/specialized renderers without uninstalling or unloading the underlying Pi package. Installing a missing feature is an explicit, warned action: the server runs npm install for the whitelisted package from localhost only, then prompts you to `/reload` the active Pi tab so newly installed resources can load.
|
|
27
|
+
|
|
28
|
+
Optional companions:
|
|
29
|
+
|
|
30
|
+
- `@firstpick/pi-prompts-git-pr` for the guided Git workflow's `/git-staged-msg` prompt.
|
|
31
|
+
- `@firstpick/pi-extension-release-npm` and `@firstpick/pi-extension-release-aur` for Publish menu commands and live release widgets.
|
|
32
|
+
- `@firstpick/pi-extension-todo-progress` for the specialized todo-progress widget.
|
|
33
|
+
- `@firstpick/pi-extension-git-footer-status` and `@firstpick/pi-extension-stats` for richer Pi status/footer and stats commands.
|
|
34
|
+
- `@firstpick/pi-themes-bundle` for theme resources used by the browser theme picker and Pi themes.
|
|
35
|
+
|
|
20
36
|
## Quick start
|
|
21
37
|
|
|
22
38
|
Install the package from npm into Pi, then restart Pi so `/webui-start` (also available as `/start-webui`) and `/webui-status` are loaded:
|
|
@@ -67,7 +83,7 @@ pi-webui --cwd /path/to/project
|
|
|
67
83
|
- Feedback reactions (`👍`, `👎`, `?`) on final assistant output plus tool/bash action cards, with queued post-run submission that asks Pi to create/update a LEARNING
|
|
68
84
|
- Basic extension UI bridge for `notify`, `setStatus`, `setWidget`, `setTitle`, `set_editor_text`, `select`, `confirm`, `input`, and `editor`
|
|
69
85
|
- Specialized `/release-npm` and `/release-aur` widget rendering with scrollable live logs plus toggle/abort actions
|
|
70
|
-
- Side-panel theme picker backed by
|
|
86
|
+
- Side-panel theme picker backed by optional `@firstpick/pi-themes-bundle` themes when loaded
|
|
71
87
|
- PWA metadata, icons, and service worker for install-to-home-screen support when served from a secure context
|
|
72
88
|
- Static frontend: no bundler, no frontend install step
|
|
73
89
|
|
|
@@ -91,7 +107,7 @@ Options:
|
|
|
91
107
|
--port <port> HTTP port (default: 31415)
|
|
92
108
|
--no-open Do not open the browser automatically
|
|
93
109
|
--no-session Start Pi RPC with --no-session
|
|
94
|
-
--name <name> Initial
|
|
110
|
+
--name <name> Initial Web UI tab display name
|
|
95
111
|
-- <pi args...> Extra arguments forwarded to Pi RPC
|
|
96
112
|
```
|
|
97
113
|
|
|
@@ -130,7 +146,7 @@ Options:
|
|
|
130
146
|
--cwd <path> Default working directory for Pi tabs (default: current dir)
|
|
131
147
|
--pi <command> Pi executable to spawn (default: bundled dependency, then "pi")
|
|
132
148
|
--no-session Start Pi RPC with --no-session
|
|
133
|
-
--name <name> Initial
|
|
149
|
+
--name <name> Initial Web UI tab display name
|
|
134
150
|
-h, --help Show help
|
|
135
151
|
-v, --version Print version
|
|
136
152
|
```
|
|
@@ -159,7 +175,7 @@ The browser button runs a native local workflow in the Web UI server process:
|
|
|
159
175
|
4. Commit with either the short message (`git commit -m ...`) or the long message (`git commit -F ...`).
|
|
160
176
|
5. Run `git push`.
|
|
161
177
|
|
|
162
|
-
This workflow
|
|
178
|
+
This workflow requires `/git-staged-msg` from `@firstpick/pi-prompts-git-pr`, which writes the two `dev/COMMIT/` files above. The Web UI enables the Git workflow button only when that command is loaded in the active Pi tab. If package resources are filtered or optional dependencies were omitted, make sure `/git-staged-msg` remains enabled. The workflow can be cancelled between steps; active native git commands are terminated on cancel where possible.
|
|
163
179
|
|
|
164
180
|
## How it works
|
|
165
181
|
|
|
@@ -172,9 +188,11 @@ pi --mode rpc
|
|
|
172
188
|
With options, each spawned command becomes:
|
|
173
189
|
|
|
174
190
|
```bash
|
|
175
|
-
pi --mode rpc [--no-session] [
|
|
191
|
+
pi --mode rpc [--no-session] [...extra Pi args]
|
|
176
192
|
```
|
|
177
193
|
|
|
194
|
+
Web UI tab titles are stored in Web UI metadata instead of being forwarded as Pi CLI `--name` flags, so multiple tabs remain compatible with bundled Pi CLI versions that do not support session naming.
|
|
195
|
+
|
|
178
196
|
The local server exposes:
|
|
179
197
|
|
|
180
198
|
- static files from `public/`
|
|
@@ -182,7 +200,8 @@ The local server exposes:
|
|
|
182
200
|
- `GET /api/directories?tab=<tabId>&path=<path>` for the browser cwd picker
|
|
183
201
|
- `GET /api/path-suggestions?tab=<tabId>&query=<path>` for `@` file/path reference autocomplete in the prompt composer
|
|
184
202
|
- `GET /api/path-fast-picks` and `POST /api/path-fast-picks` for cwd picker fast picks persisted across browser tabs, Pi terminal tabs, and Web UI server restarts
|
|
185
|
-
- `GET /api/themes` for
|
|
203
|
+
- `GET /api/themes` for optional theme data from `@firstpick/pi-themes-bundle` when available
|
|
204
|
+
- localhost-only `POST /api/optional-feature-install` for explicit, warned installation of whitelisted optional feature packages
|
|
186
205
|
- `GET /api/network` and localhost-only `POST /api/network/open` for local-network exposure status/control
|
|
187
206
|
- `GET /api/webui-status?detailed=1` for slash-command status reporting
|
|
188
207
|
- `POST /api/shutdown` for localhost-only graceful restarts from `/webui-start`/`/start-webui`; restart captures detailed tab status first so open and recently closed tabs can be restored with their session files
|
package/bin/pi-webui.mjs
CHANGED
|
@@ -104,6 +104,15 @@ const NATIVE_SLASH_COMMANDS = [
|
|
|
104
104
|
{ name: "quit", description: "Quit Pi" },
|
|
105
105
|
].map((command) => ({ ...command, source: "native", location: "Pi" }));
|
|
106
106
|
const NATIVE_SLASH_COMMAND_NAMES = new Set(NATIVE_SLASH_COMMANDS.map((command) => command.name));
|
|
107
|
+
const OPTIONAL_FEATURE_PACKAGES = new Map([
|
|
108
|
+
["gitWorkflow", "@firstpick/pi-prompts-git-pr"],
|
|
109
|
+
["releaseNpm", "@firstpick/pi-extension-release-npm"],
|
|
110
|
+
["releaseAur", "@firstpick/pi-extension-release-aur"],
|
|
111
|
+
["todoProgressWidget", "@firstpick/pi-extension-todo-progress"],
|
|
112
|
+
["gitFooterStatus", "@firstpick/pi-extension-git-footer-status"],
|
|
113
|
+
["statsCommand", "@firstpick/pi-extension-stats"],
|
|
114
|
+
["themeBundle", "@firstpick/pi-themes-bundle"],
|
|
115
|
+
]);
|
|
107
116
|
|
|
108
117
|
function usage() {
|
|
109
118
|
console.log(`pi-webui ${packageJson.version}
|
|
@@ -119,7 +128,7 @@ Options:
|
|
|
119
128
|
--cwd <path> Working directory for the Pi session (default: current dir)
|
|
120
129
|
--pi <command> Pi executable to spawn (default: bundled dependency, then "pi")
|
|
121
130
|
--no-session Start Pi RPC with --no-session
|
|
122
|
-
--name <name> Initial
|
|
131
|
+
--name <name> Initial Web UI tab display name
|
|
123
132
|
-h, --help Show this help
|
|
124
133
|
-v, --version Print version
|
|
125
134
|
|
|
@@ -232,6 +241,10 @@ function sanitizeError(error) {
|
|
|
232
241
|
return error.stack || error.message || String(error);
|
|
233
242
|
}
|
|
234
243
|
|
|
244
|
+
function delay(ms) {
|
|
245
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
246
|
+
}
|
|
247
|
+
|
|
235
248
|
class PiRpcProcess {
|
|
236
249
|
constructor({ command, args, displayCommand, cwd }) {
|
|
237
250
|
this.command = command;
|
|
@@ -274,6 +287,10 @@ class PiRpcProcess {
|
|
|
274
287
|
this.emit({ type: "pi_process_start", pid: this.child.pid, cwd: this.cwd, command: this.displayCommand, args: this.args });
|
|
275
288
|
}
|
|
276
289
|
|
|
290
|
+
isRunning() {
|
|
291
|
+
return !!this.child && this.child.exitCode === null && !this.child.killed;
|
|
292
|
+
}
|
|
293
|
+
|
|
277
294
|
onEvent(listener) {
|
|
278
295
|
this.listeners.add(listener);
|
|
279
296
|
return () => this.listeners.delete(listener);
|
|
@@ -344,7 +361,7 @@ class PiRpcProcess {
|
|
|
344
361
|
}
|
|
345
362
|
|
|
346
363
|
send(command, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
347
|
-
if (!this.
|
|
364
|
+
if (!this.isRunning() || !this.child?.stdin) {
|
|
348
365
|
return Promise.reject(new Error("Pi RPC process is not running"));
|
|
349
366
|
}
|
|
350
367
|
|
|
@@ -367,7 +384,7 @@ class PiRpcProcess {
|
|
|
367
384
|
}
|
|
368
385
|
|
|
369
386
|
async writeRaw(command) {
|
|
370
|
-
if (!this.
|
|
387
|
+
if (!this.isRunning() || !this.child?.stdin) {
|
|
371
388
|
throw new Error("Pi RPC process is not running");
|
|
372
389
|
}
|
|
373
390
|
|
|
@@ -634,6 +651,49 @@ function runCommand(command, args, { cwd, timeoutMs = 2000, maxOutputLength = 20
|
|
|
634
651
|
});
|
|
635
652
|
}
|
|
636
653
|
|
|
654
|
+
function optionalDependencyInstallRoot() {
|
|
655
|
+
const parts = packageRoot.split(path.sep);
|
|
656
|
+
const nodeModulesIndex = parts.lastIndexOf("node_modules");
|
|
657
|
+
if (nodeModulesIndex >= 0) {
|
|
658
|
+
const root = parts.slice(0, nodeModulesIndex).join(path.sep);
|
|
659
|
+
return root || path.parse(packageRoot).root;
|
|
660
|
+
}
|
|
661
|
+
return packageRoot;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function formatCommandForDisplay(command, args) {
|
|
665
|
+
return [command, ...args].map((part) => (/\s/.test(part) ? JSON.stringify(part) : part)).join(" ");
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
async function installOptionalFeaturePackage(featureId) {
|
|
669
|
+
const packageName = OPTIONAL_FEATURE_PACKAGES.get(featureId);
|
|
670
|
+
if (!packageName) throw makeHttpError(400, `Unknown optional feature: ${featureId}`);
|
|
671
|
+
|
|
672
|
+
const installRoot = optionalDependencyInstallRoot();
|
|
673
|
+
const npmCommand = process.env.PI_WEBUI_NPM_BIN || "npm";
|
|
674
|
+
const args = ["install", "--prefix", installRoot, packageName];
|
|
675
|
+
const result = await runCommand(npmCommand, args, {
|
|
676
|
+
cwd: installRoot,
|
|
677
|
+
timeoutMs: 5 * 60 * 1000,
|
|
678
|
+
maxOutputLength: 80000,
|
|
679
|
+
});
|
|
680
|
+
const command = formatCommandForDisplay(npmCommand, args);
|
|
681
|
+
const ok = result.exitCode === 0 && !result.timedOut && !result.error;
|
|
682
|
+
if (!ok) {
|
|
683
|
+
const details = [result.error, result.timedOut ? "timed out" : undefined, result.stderr?.trim(), result.stdout?.trim()].filter(Boolean).join("\n");
|
|
684
|
+
throw makeHttpError(500, `Optional feature install failed: ${command}${details ? `\n${details}` : ""}`);
|
|
685
|
+
}
|
|
686
|
+
return {
|
|
687
|
+
featureId,
|
|
688
|
+
packageName,
|
|
689
|
+
installRoot,
|
|
690
|
+
command,
|
|
691
|
+
stdout: result.stdout,
|
|
692
|
+
stderr: result.stderr,
|
|
693
|
+
message: `Installed optional feature package ${packageName}. Reload the active Pi tab to load new resources.`,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
637
697
|
function displayPath(cwd) {
|
|
638
698
|
const normalized = cwd.replace(/\\/g, "/");
|
|
639
699
|
const home = (process.env.USERPROFILE || process.env.HOME || "").replace(/\\/g, "/");
|
|
@@ -809,9 +869,9 @@ function resolveScopedModelsFromPatterns(patterns, models) {
|
|
|
809
869
|
async function getScopedModelData(tab) {
|
|
810
870
|
const { patterns, source } = await configuredScopedModelPatterns(tab.cwd);
|
|
811
871
|
if (!patterns.length) return { models: [], patterns, source };
|
|
812
|
-
const response = await tab
|
|
872
|
+
const response = await safeRpcResponse(tab, { type: "get_available_models" });
|
|
813
873
|
if (response.success === false) throw makeHttpError(400, response.error || "failed to load available models");
|
|
814
|
-
return { models: resolveScopedModelsFromPatterns(patterns, response.data?.models || []), patterns, source };
|
|
874
|
+
return { models: resolveScopedModelsFromPatterns(patterns, response.data?.models || []), patterns, source, rpcRunning: response.rpcRunning !== false };
|
|
815
875
|
}
|
|
816
876
|
|
|
817
877
|
function pathPickerRoots(activeCwd, viewedCwd) {
|
|
@@ -1449,9 +1509,9 @@ function buildPiArgsForTab(tabIndex, title) {
|
|
|
1449
1509
|
const args = ["--mode", "rpc"];
|
|
1450
1510
|
if (options.noSession) args.push("--no-session");
|
|
1451
1511
|
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1512
|
+
// Keep tab naming inside Web UI metadata. Some bundled Pi CLI versions do not
|
|
1513
|
+
// support --name, and passing Web UI-generated tab titles through to child
|
|
1514
|
+
// RPC processes makes every tab after the first exit immediately.
|
|
1455
1515
|
args.push(...options.piArgs);
|
|
1456
1516
|
return args;
|
|
1457
1517
|
}
|
|
@@ -1729,6 +1789,18 @@ function defaultTabTitle(tabIndex) {
|
|
|
1729
1789
|
return `Terminal ${tabIndex}`;
|
|
1730
1790
|
}
|
|
1731
1791
|
|
|
1792
|
+
async function primeTabRpc(tab) {
|
|
1793
|
+
try {
|
|
1794
|
+
const response = await tab.rpc.send({ type: "get_state" }, 1500);
|
|
1795
|
+
if (response.success !== false) {
|
|
1796
|
+
rememberTabState(tab, response.data);
|
|
1797
|
+
reconcileTabActivityFromState(tab, response.data);
|
|
1798
|
+
}
|
|
1799
|
+
} catch (error) {
|
|
1800
|
+
if (!/Timed out waiting for RPC response/i.test(sanitizeError(error))) throw error;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1732
1804
|
function attachRpcToTab(tab, rpc) {
|
|
1733
1805
|
tab.rpcUnsubscribe?.();
|
|
1734
1806
|
tab.rpc = rpc;
|
|
@@ -1777,6 +1849,15 @@ async function createTab({ id: requestedId, index, title, titleSource, conversat
|
|
|
1777
1849
|
attachRpcToTab(tab, rpc);
|
|
1778
1850
|
tabs.set(id, tab);
|
|
1779
1851
|
rpc.start();
|
|
1852
|
+
try {
|
|
1853
|
+
await primeTabRpc(tab);
|
|
1854
|
+
} catch (error) {
|
|
1855
|
+
if (!tab.rpc.isRunning()) {
|
|
1856
|
+
tab.rpcUnsubscribe?.();
|
|
1857
|
+
tabs.delete(id);
|
|
1858
|
+
throw new Error(`Pi RPC process failed while starting ${tabTitle}: ${sanitizeError(error)}`);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1780
1861
|
if (sessionFile && !options.noSession) {
|
|
1781
1862
|
recordEvent({ type: "webui_tab_restored", tabId: tab.id, tabTitle: tab.title, cwd: tab.cwd });
|
|
1782
1863
|
}
|
|
@@ -1799,7 +1880,7 @@ function tabMeta(tab) {
|
|
|
1799
1880
|
createdAt: tab.createdAt,
|
|
1800
1881
|
startedAt: tab.rpc.startedAt,
|
|
1801
1882
|
pid: tab.rpc.child?.pid,
|
|
1802
|
-
running:
|
|
1883
|
+
running: tab.rpc.isRunning(),
|
|
1803
1884
|
command: tab.rpc.displayCommand,
|
|
1804
1885
|
clientCount: tab.sseClients.size,
|
|
1805
1886
|
pendingExtensionUiRequestCount: pendingExtensionUiRequests(tab).length,
|
|
@@ -1968,10 +2049,67 @@ async function restartTabRpc(tab, reason = "reload") {
|
|
|
1968
2049
|
return tab;
|
|
1969
2050
|
}
|
|
1970
2051
|
|
|
2052
|
+
function rpcUnavailableMessage(tab) {
|
|
2053
|
+
return `Pi RPC process for ${tab?.title || "terminal"} is not running`;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
function fallbackRpcResponse(tab, command, error) {
|
|
2057
|
+
const message = sanitizeError(error) || rpcUnavailableMessage(tab);
|
|
2058
|
+
const base = { type: "response", command: command.type, success: true, rpcRunning: false, error: message };
|
|
2059
|
+
switch (command.type) {
|
|
2060
|
+
case "get_state":
|
|
2061
|
+
return {
|
|
2062
|
+
...base,
|
|
2063
|
+
data: {
|
|
2064
|
+
model: null,
|
|
2065
|
+
thinkingLevel: "off",
|
|
2066
|
+
isStreaming: false,
|
|
2067
|
+
isCompacting: false,
|
|
2068
|
+
steeringMode: "one-at-a-time",
|
|
2069
|
+
followUpMode: "one-at-a-time",
|
|
2070
|
+
sessionFile: tab?.sessionFile,
|
|
2071
|
+
sessionId: tab?.id,
|
|
2072
|
+
sessionName: tab?.title,
|
|
2073
|
+
autoCompactionEnabled: false,
|
|
2074
|
+
messageCount: 0,
|
|
2075
|
+
pendingMessageCount: 0,
|
|
2076
|
+
rpcRunning: false,
|
|
2077
|
+
rpcError: message,
|
|
2078
|
+
},
|
|
2079
|
+
};
|
|
2080
|
+
case "get_messages":
|
|
2081
|
+
return { ...base, data: { messages: [] } };
|
|
2082
|
+
case "get_available_models":
|
|
2083
|
+
return { ...base, data: { models: [] } };
|
|
2084
|
+
case "get_session_stats":
|
|
2085
|
+
return { ...base, data: null };
|
|
2086
|
+
case "get_last_assistant_text":
|
|
2087
|
+
return { ...base, data: { text: "" } };
|
|
2088
|
+
default:
|
|
2089
|
+
return { ...base, success: false, error: message };
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
async function safeRpcResponse(tab, command, timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
2094
|
+
try {
|
|
2095
|
+
return await tab.rpc.send(command, timeoutMs);
|
|
2096
|
+
} catch (error) {
|
|
2097
|
+
const message = sanitizeError(error);
|
|
2098
|
+
if (/Pi RPC process is not running/i.test(message)) return fallbackRpcResponse(tab, command, error);
|
|
2099
|
+
throw error;
|
|
2100
|
+
}
|
|
2101
|
+
}
|
|
2102
|
+
|
|
1971
2103
|
async function getCommandData(tab) {
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
2104
|
+
try {
|
|
2105
|
+
const response = await tab.rpc.send({ type: "get_commands" });
|
|
2106
|
+
if (response.success === false) throw makeHttpError(400, response.error || "failed to load commands");
|
|
2107
|
+
return { commands: [...NATIVE_SLASH_COMMANDS, ...(response.data?.commands || [])], rpcRunning: true };
|
|
2108
|
+
} catch (error) {
|
|
2109
|
+
const message = sanitizeError(error);
|
|
2110
|
+
if (!/Pi RPC process is not running/i.test(message)) throw error;
|
|
2111
|
+
return { commands: [...NATIVE_SLASH_COMMANDS], rpcRunning: false, error: message };
|
|
2112
|
+
}
|
|
1975
2113
|
}
|
|
1976
2114
|
|
|
1977
2115
|
function formatSessionOutput(tab, state, stats) {
|
|
@@ -2080,6 +2218,23 @@ async function closeTab(id) {
|
|
|
2080
2218
|
return tab;
|
|
2081
2219
|
}
|
|
2082
2220
|
|
|
2221
|
+
async function closeTabs(ids) {
|
|
2222
|
+
const uniqueIds = [...new Set((Array.isArray(ids) ? ids : []).map((id) => String(id || "").trim()).filter(Boolean))];
|
|
2223
|
+
const targetTabs = uniqueIds.map((id) => tabs.get(id)).filter(Boolean);
|
|
2224
|
+
if (!targetTabs.length) return [];
|
|
2225
|
+
|
|
2226
|
+
if (targetTabs.length >= tabs.size) {
|
|
2227
|
+
await createTab({ cwd: targetTabs[0]?.cwd || options.cwd });
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
const closed = [];
|
|
2231
|
+
for (const tab of targetTabs) {
|
|
2232
|
+
if (!tabs.has(tab.id)) continue;
|
|
2233
|
+
closed.push(await closeTab(tab.id));
|
|
2234
|
+
}
|
|
2235
|
+
return closed;
|
|
2236
|
+
}
|
|
2237
|
+
|
|
2083
2238
|
function requestedTabId(req, url, body) {
|
|
2084
2239
|
const header = req.headers["x-pi-webui-tab"];
|
|
2085
2240
|
const headerValue = Array.isArray(header) ? header[0] : header;
|
|
@@ -2362,6 +2517,13 @@ const server = createServer(async (req, res) => {
|
|
|
2362
2517
|
return;
|
|
2363
2518
|
}
|
|
2364
2519
|
|
|
2520
|
+
if (url.pathname === "/api/tabs/close" && req.method === "POST") {
|
|
2521
|
+
const body = await readJsonBody(req);
|
|
2522
|
+
const closed = await closeTabs(body.ids || body.tabIds || []);
|
|
2523
|
+
sendJson(res, 200, { ok: true, data: { closedIds: closed.map((tab) => tab.id), tabs: listTabs(), activeTabId: firstTab()?.id || null } });
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2365
2527
|
if (url.pathname.startsWith("/api/tabs/") && req.method === "PATCH") {
|
|
2366
2528
|
const id = decodeURIComponent(url.pathname.slice("/api/tabs/".length));
|
|
2367
2529
|
const body = await readJsonBody(req);
|
|
@@ -2511,6 +2673,14 @@ const server = createServer(async (req, res) => {
|
|
|
2511
2673
|
return;
|
|
2512
2674
|
}
|
|
2513
2675
|
|
|
2676
|
+
if (url.pathname === "/api/optional-feature-install" && req.method === "POST") {
|
|
2677
|
+
if (!isLocalAddress(req.socket.remoteAddress)) throw makeHttpError(403, "Installing optional Web UI features is only allowed from localhost");
|
|
2678
|
+
const body = await readJsonBody(req);
|
|
2679
|
+
const data = await installOptionalFeaturePackage(String(body.featureId || ""));
|
|
2680
|
+
sendJson(res, 200, { ok: true, data });
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2514
2684
|
if (url.pathname === "/api/commands" && req.method === "GET") {
|
|
2515
2685
|
const tab = getRequestedTab(req, url);
|
|
2516
2686
|
sendJson(res, 200, { type: "response", command: "get_commands", success: true, data: await getCommandData(tab) });
|
|
@@ -2558,7 +2728,7 @@ const server = createServer(async (req, res) => {
|
|
|
2558
2728
|
const getCommand = req.method === "GET" ? commandFromGet(url.pathname) : undefined;
|
|
2559
2729
|
if (getCommand) {
|
|
2560
2730
|
const tab = getRequestedTab(req, url);
|
|
2561
|
-
const response = await tab
|
|
2731
|
+
const response = await safeRpcResponse(tab, getCommand);
|
|
2562
2732
|
sendJson(res, response.success === false ? 400 : 200, response);
|
|
2563
2733
|
return;
|
|
2564
2734
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firstpick/pi-package-webui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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",
|
|
@@ -15,7 +15,29 @@
|
|
|
15
15
|
],
|
|
16
16
|
"pi": {
|
|
17
17
|
"extensions": [
|
|
18
|
-
"./index.ts"
|
|
18
|
+
"./index.ts",
|
|
19
|
+
"../pi-extension-git-footer-status/index.ts",
|
|
20
|
+
"../pi-extension-release-aur/index.ts",
|
|
21
|
+
"../pi-extension-release-npm/index.ts",
|
|
22
|
+
"../pi-extension-stats/index.ts",
|
|
23
|
+
"../pi-extension-todo-progress/index.ts",
|
|
24
|
+
"node_modules/@firstpick/pi-extension-git-footer-status/index.ts",
|
|
25
|
+
"node_modules/@firstpick/pi-extension-release-aur/index.ts",
|
|
26
|
+
"node_modules/@firstpick/pi-extension-release-npm/index.ts",
|
|
27
|
+
"node_modules/@firstpick/pi-extension-stats/index.ts",
|
|
28
|
+
"node_modules/@firstpick/pi-extension-todo-progress/index.ts"
|
|
29
|
+
],
|
|
30
|
+
"skills": [
|
|
31
|
+
"../pi-extension-release-aur/skills",
|
|
32
|
+
"node_modules/@firstpick/pi-extension-release-aur/skills"
|
|
33
|
+
],
|
|
34
|
+
"prompts": [
|
|
35
|
+
"../pi-package-prompts-git-pr/prompts",
|
|
36
|
+
"node_modules/@firstpick/pi-prompts-git-pr/prompts"
|
|
37
|
+
],
|
|
38
|
+
"themes": [
|
|
39
|
+
"../pi-package-themes-bundle/themes",
|
|
40
|
+
"node_modules/@firstpick/pi-themes-bundle/themes"
|
|
19
41
|
]
|
|
20
42
|
},
|
|
21
43
|
"bin": {
|
|
@@ -26,8 +48,16 @@
|
|
|
26
48
|
"test": "node tests/mobile-static.test.mjs"
|
|
27
49
|
},
|
|
28
50
|
"dependencies": {
|
|
29
|
-
"@earendil-works/pi-coding-agent": "^0.78.0"
|
|
30
|
-
|
|
51
|
+
"@earendil-works/pi-coding-agent": "^0.78.0"
|
|
52
|
+
},
|
|
53
|
+
"optionalDependencies": {
|
|
54
|
+
"@firstpick/pi-extension-git-footer-status": "^0.2.1",
|
|
55
|
+
"@firstpick/pi-extension-release-aur": "^0.1.3",
|
|
56
|
+
"@firstpick/pi-extension-release-npm": "^0.3.3",
|
|
57
|
+
"@firstpick/pi-extension-stats": "^0.2.0",
|
|
58
|
+
"@firstpick/pi-extension-todo-progress": "^0.1.7",
|
|
59
|
+
"@firstpick/pi-prompts-git-pr": "^0.1.0",
|
|
60
|
+
"@firstpick/pi-themes-bundle": "^0.1.1"
|
|
31
61
|
},
|
|
32
62
|
"files": [
|
|
33
63
|
"index.ts",
|