@jmfederico/pi-web 1.202605.11 → 1.202605.13
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 +47 -50
- package/dist/cli.js +514 -115
- package/dist/cli.js.map +1 -1
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/{CodeViewer-0NuLmnBL.js → CodeViewer-ncgSvOQM.js} +1 -1
- package/dist/client/assets/TerminalPanel-DjuVNsp5.js +75 -0
- package/dist/client/assets/index-CDGTKzLD.js +1239 -0
- package/dist/client/favicon.svg +11 -0
- package/dist/client/index.html +6 -2
- package/dist/client/manifest.webmanifest +24 -0
- package/dist/client/pwa-icon-192.png +0 -0
- package/dist/client/pwa-icon-512.png +0 -0
- package/dist/config.js +4 -4
- package/dist/pi-web-plugins/pi-web/pi-web-plugin.js +13 -9
- package/dist/server/diagnostics/nodePtySpawnHelper.js +135 -0
- package/dist/server/diagnostics/nodePtySpawnHelper.js.map +1 -0
- package/dist/server/piWebPluginService.js +9 -9
- package/dist/server/piWebStatus.js +8 -8
- package/dist/server/piWebStatus.js.map +1 -1
- package/dist/server/sessions/piSessionService.js +169 -19
- package/dist/server/sessions/piSessionService.js.map +1 -1
- package/dist/server/sessions/sessionArchiveTree.js +35 -0
- package/dist/server/sessions/sessionArchiveTree.js.map +1 -0
- package/dist/server/sessions/sessionCommandService.js +88 -2
- package/dist/server/sessions/sessionCommandService.js.map +1 -1
- package/dist/server/sessions/sessionRoutes.js +8 -0
- package/dist/server/sessions/sessionRoutes.js.map +1 -1
- package/dist/server/terminalProxyRoutes.js +70 -0
- package/dist/server/terminalProxyRoutes.js.map +1 -1
- package/dist/server/terminals/terminalRoutes.js +65 -1
- package/dist/server/terminals/terminalRoutes.js.map +1 -1
- package/dist/server/terminals/terminalService.js +202 -37
- package/dist/server/terminals/terminalService.js.map +1 -1
- package/dist/server/workspaceExplorerRoutes.js +18 -0
- package/dist/server/workspaceExplorerRoutes.js.map +1 -1
- package/dist/server/workspaces/fileContentService.js +23 -3
- package/dist/server/workspaces/fileContentService.js.map +1 -1
- package/dist/server/workspaces/imagePreviewService.js +40 -0
- package/dist/server/workspaces/imagePreviewService.js.map +1 -0
- package/dist/shared/workspaceFiles.js +3 -0
- package/dist/shared/workspaceFiles.js.map +1 -0
- package/docs/assets/favicon.svg +11 -0
- package/docs/plugins.md +43 -42
- package/extensions/pi-web.ts +12 -5
- package/package.json +1 -1
- package/plugin-api.d.ts +2 -0
- package/dist/client/assets/TerminalPanel-BG6_9svt.js +0 -47
- package/dist/client/assets/index-DmVN7DQr.js +0 -1143
package/docs/plugins.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# PI WEB plugin API
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
PI WEB plugins are trusted browser-side ES modules that extend the PI WEB UI. They are intended for personal, team, and project-local customization, and simple enough for an LLM to create or modify directly.
|
|
4
4
|
|
|
5
5
|
Plugins can currently:
|
|
6
6
|
|
|
7
7
|
- add action-palette commands;
|
|
8
8
|
- add workspace tools/panels next to Files, Git, and Terminal;
|
|
9
9
|
- add compact workspace-label items in the workspace list, panel header, and status bar;
|
|
10
|
-
- call browser APIs and
|
|
10
|
+
- call browser APIs and PI WEB HTTP/WebSocket APIs available to the current browser session;
|
|
11
11
|
- serve their own static assets from the plugin directory.
|
|
12
12
|
|
|
13
13
|
They do **not** run in the session daemon, do not get a server-side hook API, and are not sandboxed.
|
|
@@ -17,8 +17,8 @@ They do **not** run in the session daemon, do not get a server-side hook API, an
|
|
|
17
17
|
Plugins run as JavaScript in the browser app. Treat them as trusted code:
|
|
18
18
|
|
|
19
19
|
- they can call browser APIs;
|
|
20
|
-
- they can `fetch()`
|
|
21
|
-
- they can read workspace files through
|
|
20
|
+
- they can `fetch()` PI WEB API endpoints using the current browser access;
|
|
21
|
+
- they can read workspace files through PI WEB's file endpoints if the UI can read them;
|
|
22
22
|
- they can render arbitrary Lit templates/custom elements in plugin contribution areas;
|
|
23
23
|
- they should not be installed from untrusted sources.
|
|
24
24
|
|
|
@@ -37,23 +37,23 @@ Good plugin requests:
|
|
|
37
37
|
Copy-paste prompt for creating a plugin:
|
|
38
38
|
|
|
39
39
|
```text
|
|
40
|
-
Build a
|
|
40
|
+
Build a PI WEB plugin for this project.
|
|
41
41
|
Goal: <describe the UI behavior>.
|
|
42
|
-
Before coding, read the
|
|
42
|
+
Before coding, read the PI WEB plugin docs:
|
|
43
43
|
https://pi-web.dev/plugins.html
|
|
44
44
|
Full API reference:
|
|
45
45
|
https://pi-web.dev/plugins.md
|
|
46
46
|
Create it as a local plugin under ~/.pi-web/plugins/<plugin-id>.
|
|
47
47
|
Use the appropriate extension points from the docs.
|
|
48
48
|
Validate by checking /pi-web-plugins/manifest.json and explain how to reload/debug it.
|
|
49
|
-
Do not modify
|
|
49
|
+
Do not modify PI WEB itself.
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
Copy-paste prompt for modifying a plugin:
|
|
53
53
|
|
|
54
54
|
```text
|
|
55
|
-
Improve the
|
|
56
|
-
Before coding, read the
|
|
55
|
+
Improve the PI WEB plugin at <path>.
|
|
56
|
+
Before coding, read the PI WEB plugin docs:
|
|
57
57
|
https://pi-web.dev/plugins.html
|
|
58
58
|
Full API reference:
|
|
59
59
|
https://pi-web.dev/plugins.md
|
|
@@ -63,9 +63,9 @@ After editing, check the manifest endpoint and browser-console failure cases.
|
|
|
63
63
|
|
|
64
64
|
## Canonical example: bundled Info plugin
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
PI WEB ships a real bundled `info` plugin. Use it as the reference example because it is intentionally small while still exercising all core contribution types: an action, a workspace label, and a workspace panel.
|
|
67
67
|
|
|
68
|
-
Bundled
|
|
68
|
+
Bundled PI WEB plugins are developed as TypeScript in the repository, but their `package.json` metadata still points at built JavaScript because plugins are loaded by the browser as JS ES modules. `npm run dev:web` watches and rebuilds bundled plugin TS into `dist/pi-web-plugins/` during development, and `npm run build` emits the JS before packaging a release.
|
|
69
69
|
|
|
70
70
|
Source files:
|
|
71
71
|
|
|
@@ -112,20 +112,20 @@ export default {
|
|
|
112
112
|
|
|
113
113
|
When copying the Info plugin, choose a new plugin id so it does not conflict with the bundled `info` plugin.
|
|
114
114
|
|
|
115
|
-
|
|
115
|
+
PI WEB also ships a `pi-web` status plugin that demonstrates dynamic `visible` and `badge` callbacks for tabs that only appear when the host has status messages or needs extra install visibility.
|
|
116
116
|
|
|
117
117
|
## Local plugin usage
|
|
118
118
|
|
|
119
|
-
This works with the production
|
|
119
|
+
This works with the production native-service install. PI WEB discovers plugins from `~/.pi-web/plugins/<plugin-package>/` on the web/API side; no PI WEB rebuild or session-daemon restart is required. If `PI_WEB_DATA_DIR` is set, use `$PI_WEB_DATA_DIR/plugins` instead.
|
|
120
120
|
|
|
121
|
-
Symlink a plugin folder into
|
|
121
|
+
Symlink a plugin folder into PI WEB's local plugin directory:
|
|
122
122
|
|
|
123
123
|
```bash
|
|
124
124
|
mkdir -p ~/.pi-web/plugins
|
|
125
125
|
ln -s /path/to/plugin-folder ~/.pi-web/plugins/plugin-id
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-
Reload the
|
|
128
|
+
Reload the PI WEB browser tab. PI WEB serves plugin modules with an mtime-based `?v=` cache buster. After editing a plugin, hard reload the browser if you do not see changes.
|
|
129
129
|
|
|
130
130
|
## First-party separate plugin packages
|
|
131
131
|
|
|
@@ -133,11 +133,11 @@ First-party plugins that are published as their own npm packages can live in thi
|
|
|
133
133
|
|
|
134
134
|
A separate plugin package should:
|
|
135
135
|
|
|
136
|
-
- use type-only imports from `@jmfederico/pi-web/plugin-api` when it needs shared
|
|
137
|
-
- keep its
|
|
136
|
+
- use type-only imports from `@jmfederico/pi-web/plugin-api` when it needs shared PI WEB plugin interfaces; this subpath is currently a `.d.ts`-only dogfooding surface, not a runtime JavaScript module;
|
|
137
|
+
- keep its PI WEB metadata in its own `package.json` with `piWeb.plugins` entries pointing at built JavaScript in `dist/`;
|
|
138
138
|
- include a package-level `build` script and `prepack` script so `npm pack --workspace <package>` and `npm publish --workspace <package>` produce a usable plugin package;
|
|
139
139
|
- use a local symlink into `~/.pi-web/plugins/<plugin-id>` while developing;
|
|
140
|
-
- document any private
|
|
140
|
+
- document any private PI WEB APIs it dogfoods until those APIs become stable plugin runtime helpers.
|
|
141
141
|
|
|
142
142
|
Typical local development loop from this repository:
|
|
143
143
|
|
|
@@ -146,13 +146,13 @@ npm run dev
|
|
|
146
146
|
curl http://127.0.0.1:8504/pi-web-plugins/manifest.json
|
|
147
147
|
```
|
|
148
148
|
|
|
149
|
-
The main
|
|
149
|
+
The main PI WEB `dev` command watches bundled plugins in `pi-web-plugins/`, builds/watches separate plugin packages in `plugins/*`, and discovers those source-checkout plugin packages without symlinking them into `~/.pi-web/plugins`.
|
|
150
150
|
|
|
151
151
|
## Discovery and packaging
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
PI WEB builds `/pi-web-plugins/manifest.json` from these sources:
|
|
154
154
|
|
|
155
|
-
1. Bundled plugins in the
|
|
155
|
+
1. Bundled plugins in the PI WEB package:
|
|
156
156
|
|
|
157
157
|
```text
|
|
158
158
|
pi-web-plugins/<plugin-package>/
|
|
@@ -166,7 +166,7 @@ Pi Web builds `/pi-web-plugins/manifest.json` from these sources:
|
|
|
166
166
|
|
|
167
167
|
Entries may be real directories or symlinks. This is the recommended development workflow.
|
|
168
168
|
|
|
169
|
-
3. Installed Pi packages that expose
|
|
169
|
+
3. Installed Pi packages that expose PI WEB plugin metadata. Pi packages may be user or project scoped.
|
|
170
170
|
|
|
171
171
|
Plugin package directory names and plugin ids must be valid identifiers:
|
|
172
172
|
|
|
@@ -174,7 +174,7 @@ Plugin package directory names and plugin ids must be valid identifiers:
|
|
|
174
174
|
^[a-z][a-z0-9.-]*$
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
-
A package can expose one or more
|
|
177
|
+
A package can expose one or more PI WEB plugin modules. There is exactly one supported `package.json` metadata shape:
|
|
178
178
|
|
|
179
179
|
```json
|
|
180
180
|
{
|
|
@@ -222,7 +222,7 @@ A plugin can fetch its own static assets with URLs under:
|
|
|
222
222
|
/pi-web-plugins/<plugin-id>/<path-inside-plugin-root>
|
|
223
223
|
```
|
|
224
224
|
|
|
225
|
-
|
|
225
|
+
PI WEB prevents asset path traversal outside the plugin root. JavaScript, JSON, CSS, and HTML get appropriate content types; other files are served as octet-stream.
|
|
226
226
|
|
|
227
227
|
## Plugin module shape
|
|
228
228
|
|
|
@@ -264,7 +264,7 @@ export default {
|
|
|
264
264
|
|
|
265
265
|
`activate()` is called once when the UI loads the plugin. Keep it cheap: define contributions there, but move expensive or async work into actions, custom elements, or explicit user interactions.
|
|
266
266
|
|
|
267
|
-
The plugin id comes from `package.json`, not from the JavaScript module. Contribution ids are local to the plugin and
|
|
267
|
+
The plugin id comes from `package.json`, not from the JavaScript module. Contribution ids are local to the plugin and PI WEB qualifies them internally as:
|
|
268
268
|
|
|
269
269
|
```text
|
|
270
270
|
<plugin-id>:<local-contribution-id>
|
|
@@ -345,17 +345,18 @@ Notes:
|
|
|
345
345
|
|
|
346
346
|
- `state` is a snapshot of current UI state when actions are built.
|
|
347
347
|
- Only `state.selectedWorkspace` and `state.selectedSession` are documented as stable for plugin authors.
|
|
348
|
-
- Other `state` fields may exist at runtime, but they are
|
|
348
|
+
- Other `state` fields may exist at runtime, but they are PI WEB internals and can change quickly.
|
|
349
349
|
- `enabled` is evaluated when the action palette asks for actions.
|
|
350
350
|
- `selectWorkspaceTool()` expects a qualified panel id such as `my-plugin:workspace.info`.
|
|
351
|
-
- `openTerminal()` switches to the built-in terminal panel. Pass `{ terminalId }` to deep-link to a specific terminal
|
|
351
|
+
- `openTerminal()` switches to the built-in terminal panel. Pass `{ terminalId }` to deep-link to a specific terminal.
|
|
352
|
+
- Only fields documented here and declared in `plugin-api.d.ts` are stable public plugin API. PI WEB may attach `piWebInternal` fields at runtime for first-party dogfooding; plugins should not depend on those fields because they can change or disappear without notice.
|
|
352
353
|
|
|
353
354
|
#### Keyboard shortcuts
|
|
354
355
|
|
|
355
|
-
- App-level keyboard shortcuts must be attached to actions.
|
|
356
|
+
- App-level keyboard shortcuts must be attached to actions. PI WEB does not support standalone plugin keyboard commands; contribute an action first, then add a `shortcut` if it needs a keybinding.
|
|
356
357
|
- `shortcut` is the action's default keybinding. It is displayed in the action palette and handled by the global shortcut dispatcher when the action is enabled.
|
|
357
358
|
- Use modified shortcuts such as `mod+shift+p`; plain letter shortcuts are intentionally ignored so normal typing is never captured.
|
|
358
|
-
- Future
|
|
359
|
+
- Future PI WEB versions may allow users to override or disable action shortcuts by action id, so plugins should treat `shortcut` as a default rather than a guaranteed final binding.
|
|
359
360
|
- Choose shortcuts carefully to avoid conflicts. There is no user-facing shortcut override or conflict resolver yet.
|
|
360
361
|
- Local text input, terminal input, list navigation, and dialog keys such as Enter, Escape, and arrow keys do not need to be plugin actions unless they are app-level commands.
|
|
361
362
|
|
|
@@ -399,7 +400,7 @@ interface WorkspacePanelContext {
|
|
|
399
400
|
}
|
|
400
401
|
```
|
|
401
402
|
|
|
402
|
-
`workspace` and `openTerminal()` are documented as stable for panel callbacks. Other fields may exist at runtime, but they are
|
|
403
|
+
`workspace` and `openTerminal()` are documented as stable for panel callbacks. Other fields may exist at runtime, but they are PI WEB internals and can change quickly. If a panel needs file, git, terminal, or session data beyond the helpers documented here, prefer explicit `fetch()` calls and keep them isolated.
|
|
403
404
|
|
|
404
405
|
Useful workspace shape:
|
|
405
406
|
|
|
@@ -420,7 +421,7 @@ Use existing classes such as `toolbar`, `viewer`, `empty`, and `muted` for panel
|
|
|
420
421
|
|
|
421
422
|
### Workspace labels
|
|
422
423
|
|
|
423
|
-
Workspace labels add compact inline metadata wherever
|
|
424
|
+
Workspace labels add compact inline metadata wherever PI WEB displays a workspace label: workspace list, workspace panel header, and status bar.
|
|
424
425
|
|
|
425
426
|
Use them for short facts like project environment, local URL, branch status, container name, or health state.
|
|
426
427
|
|
|
@@ -456,7 +457,7 @@ interface WorkspaceLabelContext {
|
|
|
456
457
|
}
|
|
457
458
|
```
|
|
458
459
|
|
|
459
|
-
Only `workspace` is documented as stable for label callbacks. Other fields may exist at runtime, but they are
|
|
460
|
+
Only `workspace` is documented as stable for label callbacks. Other fields may exist at runtime, but they are PI WEB internals and can change quickly.
|
|
460
461
|
|
|
461
462
|
Items are sorted by `order` and then id. Return an empty array to render nothing.
|
|
462
463
|
|
|
@@ -478,7 +479,7 @@ Items are sorted by `order` and then id. Return an empty array to render nothing
|
|
|
478
479
|
}
|
|
479
480
|
```
|
|
480
481
|
|
|
481
|
-
|
|
482
|
+
PI WEB renders the anchor and adds safe defaults such as `rel="noopener noreferrer"` for `_blank` links. `javascript:` and `data:` links are rendered as plain text instead of links.
|
|
482
483
|
|
|
483
484
|
#### Render items
|
|
484
485
|
|
|
@@ -518,7 +519,7 @@ export default {
|
|
|
518
519
|
|
|
519
520
|
## Reading workspace files
|
|
520
521
|
|
|
521
|
-
Plugins can use existing
|
|
522
|
+
Plugins can use existing PI WEB endpoints. For example, to read a file in a workspace:
|
|
522
523
|
|
|
523
524
|
```js
|
|
524
525
|
async function readWorkspaceFile(workspace, path) {
|
|
@@ -533,13 +534,13 @@ async function readWorkspaceFile(workspace, path) {
|
|
|
533
534
|
}
|
|
534
535
|
```
|
|
535
536
|
|
|
536
|
-
The file response includes fields such as `path`, `content`, `truncated`, and `binary`, but endpoint response shapes are private
|
|
537
|
+
The file response includes fields such as `path`, `content`, `truncated`, and `binary`, but endpoint response shapes are private PI WEB implementation details for now and can change between releases.
|
|
537
538
|
|
|
538
539
|
Be careful with sensitive files such as `.env`: plugins are trusted browser code, and file contents are exposed to the plugin.
|
|
539
540
|
|
|
540
|
-
## Other useful
|
|
541
|
+
## Other useful PI WEB APIs
|
|
541
542
|
|
|
542
|
-
Plugins may call any endpoint available to the browser, but these HTTP endpoints are considered private
|
|
543
|
+
Plugins may call any endpoint available to the browser, but these HTTP endpoints are considered private PI WEB implementation APIs for now. They can change quickly between releases. Prefer plugin runtime context helpers when they cover the interaction, and keep any direct HTTP usage small and isolated.
|
|
543
544
|
|
|
544
545
|
Common read endpoints:
|
|
545
546
|
|
|
@@ -566,11 +567,11 @@ POST /api/sessions/:id/archive
|
|
|
566
567
|
POST /api/sessions/:id/restore
|
|
567
568
|
```
|
|
568
569
|
|
|
569
|
-
Prefer runtime context helpers (`startSession`, `stopActiveWork`, `refreshFiles`, `refreshGit`, etc.) when they cover the interaction. Use direct HTTP calls only for plugin-specific data or behavior, and expect to update them as
|
|
570
|
+
Prefer runtime context helpers (`startSession`, `stopActiveWork`, `refreshFiles`, `refreshGit`, etc.) when they cover the interaction. Use direct HTTP calls only for plugin-specific data or behavior, and expect to update them as PI WEB evolves.
|
|
570
571
|
|
|
571
572
|
## Async data and caching
|
|
572
573
|
|
|
573
|
-
|
|
574
|
+
PI WEB does not provide a plugin cache/invalidation framework. Keep host callbacks cheap:
|
|
574
575
|
|
|
575
576
|
- simple contributions should be synchronous and cheap;
|
|
576
577
|
- expensive or async work should live inside the plugin;
|
|
@@ -580,7 +581,7 @@ Pi Web does not provide a plugin cache/invalidation framework. Keep host callbac
|
|
|
580
581
|
|
|
581
582
|
## Agent implementation checklist
|
|
582
583
|
|
|
583
|
-
If you are an AI agent building or editing a
|
|
584
|
+
If you are an AI agent building or editing a PI WEB plugin, follow this checklist:
|
|
584
585
|
|
|
585
586
|
1. Create or update a plugin folder with `package.json` and a JavaScript module such as `pi-web-plugin.js`.
|
|
586
587
|
2. Use the single supported package metadata shape: `piWeb.plugins` array with `{ id, module }` entries.
|
|
@@ -594,7 +595,7 @@ If you are an AI agent building or editing a Pi Web plugin, follow this checklis
|
|
|
594
595
|
10. Add workspace labels for compact inline metadata.
|
|
595
596
|
11. Return arrays from workspace label `items()`; return an empty array to render nothing.
|
|
596
597
|
12. Use stable context fields first; only `workspace`, `state.selectedWorkspace`, and `state.selectedSession` are documented as stable.
|
|
597
|
-
13. Use `fetch()` against
|
|
598
|
+
13. Use `fetch()` against PI WEB APIs only for plugin-specific behavior not provided by runtime context helpers, and isolate those calls because HTTP endpoints are private for now.
|
|
598
599
|
14. Treat plugins as trusted code and avoid reading or displaying secrets unless intentional.
|
|
599
600
|
15. After local edits, tell the user to hard reload the browser and check the console for plugin errors.
|
|
600
601
|
|
package/extensions/pi-web.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
3
4
|
import { dirname, join } from "node:path";
|
|
4
5
|
import { fileURLToPath } from "node:url";
|
|
5
6
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
7
|
|
|
7
8
|
const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
8
9
|
const cliPath = join(packageRoot, "dist", "cli.js");
|
|
9
|
-
const serviceNames = ["pi-web-sessiond.service", "pi-web.service"];
|
|
10
|
+
const serviceNames = ["pi-web-sessiond.service", "pi-web.service", "pi-web-ui-dev.service"];
|
|
11
|
+
const macLogPaths = ["sessiond.log", "web.log", "ui-dev.log"].map((name) => join(homedir(), ".pi-web", "logs", name));
|
|
10
12
|
|
|
11
13
|
const subcommands = [
|
|
12
14
|
"install",
|
|
@@ -76,12 +78,17 @@ function isSubcommand(value: string): value is Subcommand {
|
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
async function boundedLogs(): Promise<{ code: number; output: string }> {
|
|
79
|
-
|
|
81
|
+
if (process.platform === "darwin") {
|
|
82
|
+
const existingLogs = macLogPaths.filter((path) => existsSync(path));
|
|
83
|
+
if (existingLogs.length === 0) return { code: 1, output: "No PI WEB log files found in ~/.pi-web/logs." };
|
|
84
|
+
return run("tail", ["-n", "100", ...existingLogs]);
|
|
85
|
+
}
|
|
86
|
+
return run("journalctl", ["--user", ...serviceNames.flatMap((serviceName) => ["-u", serviceName]), "-n", "100", "--no-pager"]);
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
export default function piWebExtension(pi: ExtensionAPI): void {
|
|
83
90
|
pi.registerCommand("pi-web", {
|
|
84
|
-
description: "Manage
|
|
91
|
+
description: "Manage PI WEB services: install, status, logs, restart, start, stop, doctor, open",
|
|
85
92
|
getArgumentCompletions(prefix: string): { value: string; label: string }[] | null {
|
|
86
93
|
const [first = ""] = parseArgs(prefix);
|
|
87
94
|
const items = subcommands
|
|
@@ -95,12 +102,12 @@ export default function piWebExtension(pi: ExtensionAPI): void {
|
|
|
95
102
|
const rest = parsedArgs.slice(1);
|
|
96
103
|
|
|
97
104
|
if (subcommand === "help") {
|
|
98
|
-
ctx.ui.notify(`
|
|
105
|
+
ctx.ui.notify(`PI WEB commands:\n\n${subcommands.map((command) => `/pi-web ${command}`).join("\n")}\n\nLogs are bounded to the last 100 service log lines in the Pi command. Use \`pi-web logs\` in a shell to follow logs.`, "info");
|
|
99
106
|
return;
|
|
100
107
|
}
|
|
101
108
|
|
|
102
109
|
if (subcommand === "open") {
|
|
103
|
-
ctx.ui.notify("
|
|
110
|
+
ctx.ui.notify("PI WEB default URL: http://127.0.0.1:8504", "info");
|
|
104
111
|
return;
|
|
105
112
|
}
|
|
106
113
|
|
package/package.json
CHANGED
package/plugin-api.d.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface PluginRuntimeContext {
|
|
|
50
50
|
openTerminal: (options?: { terminalId?: string | undefined }) => void;
|
|
51
51
|
refreshFiles: () => void | Promise<void>;
|
|
52
52
|
refreshGit: () => void | Promise<void>;
|
|
53
|
+
refreshAppData: () => void | Promise<void>;
|
|
54
|
+
reloadPage: () => void;
|
|
53
55
|
startSession: () => void | Promise<void>;
|
|
54
56
|
archiveSession: () => void | Promise<void>;
|
|
55
57
|
stopActiveWork: () => void | Promise<void>;
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import{i as T,c as h,s as x,d as u,f as y,b as c,a as I,n as p,e as k,r as d,t as S}from"./index-DmVN7DQr.js";import{D as f,o as v}from"./vendor-terminal-DjQ08hXu.js";import"./vendor-editor-core-B4Sq6exx.js";import"./vendor-editor-languages-DznYbTkJ.js";var C=Object.defineProperty,O=Object.getOwnPropertyDescriptor,a=(e,t,i,r)=>{for(var s=r>1?void 0:r?O(t,i):t,n=e.length-1,l;n>=0;n--)(l=e[n])&&(s=(r?l(t,i,s):l(s))||s);return r&&s&&C(t,i,s),s};const A={cursorBlink:!0,convertEol:!0,fontFamily:"ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",fontSize:13},z={cols:100,rows:30};let o=class extends T{constructor(){super(...arguments),this.autoStart=!1,this.onSelectTerminal=()=>{},this.terminals=[],this.loading=!1,this.visible=!1,this.suppressTerminalInput=!1}connectedCallback(){super.connectedCallback(),this.themeObserver=new MutationObserver(()=>{this.applyTerminalTheme()}),this.themeObserver.observe(document.documentElement,{attributes:!0,attributeFilter:["class","style","data-theme"]})}firstUpdated(){this.intersectionObserver=new IntersectionObserver(e=>{this.visible=e[0]?.isIntersecting===!0}),this.intersectionObserver.observe(this)}disconnectedCallback(){this.intersectionObserver?.disconnect(),this.intersectionObserver=void 0,this.themeObserver?.disconnect(),this.themeObserver=void 0,this.disposeTerminalView(),super.disconnectedCallback()}willUpdate(e){const t=this.workspace?.path;if(t!==this.observedCwd){this.observedCwd=t,this.loadedCwd=void 0,this.autoStartConsumedCwd=void 0,this.terminals=[],this.selectedId=void 0,this.disposeTerminalView();return}if(e.has("selectedTerminalId")){if(e.get("selectedTerminalId")!==void 0&&this.selectedTerminalId===void 0){this.loadedCwd=void 0,this.selectTerminalIdInView(void 0);return}this.applyRequestedTerminalSelection()}}updated(e){this.loadVisibleWorkspaceTerminals(),e.has("selectedTerminalId")&&this.shouldReloadForRequestedTerminal()&&this.loadTerminals(),this.ensureTerminalView()}loadVisibleWorkspaceTerminals(){const e=this.workspace?.path;!this.visible||e===void 0||e===this.loadedCwd||(this.loadedCwd=e,this.loadTerminals())}async loadTerminals(){this.loading=!0,this.error=void 0;try{if(this.workspace===void 0)return;const e=this.consumeAutoStart(),t=await h.terminals(this.workspace.projectId,this.workspace.id);this.terminals=t,this.selectPreferredLoadedTerminal({replaceUrl:!0}),t.length===0&&e&&await this.startTerminal()}catch(e){this.error=e instanceof Error?e.message:String(e)}finally{this.loading=!1}}applyRequestedTerminalSelection(){this.selectedTerminalId!==void 0&&!this.terminals.some(e=>e.id===this.selectedTerminalId)||this.selectPreferredLoadedTerminal({replaceUrl:!0})}consumeAutoStart(){const e=this.workspace?.path;return!this.autoStart||e===void 0||this.autoStartConsumedCwd===e?!1:(this.autoStartConsumedCwd=e,!0)}shouldReloadForRequestedTerminal(){const e=this.workspace?.path;return this.visible&&e!==void 0&&e===this.loadedCwd&&this.selectedTerminalId!==void 0&&!this.loading&&!this.terminals.some(t=>t.id===this.selectedTerminalId)}selectPreferredLoadedTerminal(e){let t=x(this.terminals,{targetTerminalId:this.selectedTerminalId});t===void 0&&this.selectedTerminalId!==void 0&&(t=u(this.terminals)),this.selectTerminalIdInView(t?.id),(t?.id!==this.selectedTerminalId||t===void 0&&this.selectedTerminalId!==void 0)&&this.onSelectTerminal(t?.id,{replace:e?.replaceUrl===!0})}selectTerminalIdInView(e){this.selectedId!==e&&(this.selectedId=e,this.disposeTerminalView())}async startTerminal(){if(this.workspace!==void 0){this.error=void 0;try{const e=this.measureTerminalSize()??z,t=await h.startTerminal(this.workspace.projectId,this.workspace.id,e);this.terminals=[...this.terminals,t],this.selectTerminal(t.id)}catch(e){this.error=e instanceof Error?e.message:String(e)}}}async closeTerminal(e,t){t.stopPropagation();try{if(this.workspace===void 0)return;await h.closeTerminal(this.workspace.projectId,this.workspace.id,e);const i=this.terminals.filter(r=>r.id!==e);if(this.terminals=i,this.selectedId===e||this.selectedTerminalId===e){const r=u(i)?.id;this.selectTerminalIdInView(r),this.onSelectTerminal(r,{replace:!0})}}catch(i){this.error=i instanceof Error?i.message:String(i)}}selectTerminal(e){this.selectedId!==e&&this.selectTerminalIdInView(e),this.onSelectTerminal(e)}ensureTerminalView(){const e=this.workspace,t=this.terminalHostElement();if(!this.visible||this.terminal!==void 0||this.selectedId===void 0||t===void 0||e===void 0)return;const i=new f(b(this)),r=new v;i.loadAddon(r),i.open(t),this.terminal=i,this.fitAddon=r,this.resizeObserver=new ResizeObserver(()=>{this.fitAndNotify()}),this.resizeObserver.observe(t),i.onData(n=>{if(this.suppressTerminalInput)return;const l=$(n);l!==""&&this.send({type:"input",data:l})});const s=this.fitTerminal();this.connectSocket(e.projectId,e.id,this.selectedId,i,s),requestAnimationFrame(()=>{this.fitAndNotify()}),i.focus()}connectSocket(e,t,i,r,s){const n=y(e,t,i,s);n.binaryType="arraybuffer",this.socket=n,n.addEventListener("open",()=>{this.fitAndNotify()}),n.addEventListener("message",l=>{this.handleSocketMessage(l.data,i,r)}),n.addEventListener("close",()=>{this.socket===n&&(this.socket=void 0)})}async handleSocketMessage(e,t,i){try{const r=E(await V(e));r.type==="output"&&this.writeTerminalOutput(i,r.data,r.replay===!0),r.type==="exit"&&(i.writeln(`\r
|
|
2
|
-
[process exited${r.exitCode===void 0?"":` with code ${String(r.exitCode)}`}]`),this.terminals=this.terminals.map(s=>s.id===t?{...s,exited:!0,...r.exitCode===void 0?{}:{exitCode:r.exitCode}}:s)),r.type==="error"&&i.writeln(`\r
|
|
3
|
-
[terminal error: ${r.message}]`)}catch(r){i.writeln(`\r
|
|
4
|
-
[terminal error: ${r instanceof Error?r.message:String(r)}]`)}}writeTerminalOutput(e,t,i){if(!i){e.write(t);return}this.suppressTerminalInput=!0,e.write(t,()=>{this.suppressTerminalInput=!1})}fitAndNotify(){const e=this.fitTerminal();e!==void 0&&this.send({type:"resize",...e})}fitTerminal(){if(this.fitAddon===void 0||this.terminal===void 0)return;const e=this.fitAddon.proposeDimensions(),t=w(e);if(t!==void 0)return this.fitAddon.fit(),t}measureTerminalSize(){const e=this.fitTerminal();if(e!==void 0)return e;const t=this.terminalHostElement();if(this.terminal!==void 0||t===void 0)return;const i=new f(b(this)),r=new v;i.loadAddon(r),i.open(t);const s=w(r.proposeDimensions());return i.dispose(),s}terminalHostElement(){const e=this.terminalHost;return e instanceof HTMLDivElement?e:void 0}applyTerminalTheme(){this.terminal!==void 0&&(this.terminal.options.theme=g(this))}send(e){this.socket?.readyState===WebSocket.OPEN&&this.socket.send(JSON.stringify(e))}disposeTerminalView(){this.resizeObserver?.disconnect(),this.resizeObserver=void 0,this.socket?.close(),this.socket=void 0,this.terminal?.dispose(),this.terminal=void 0,this.fitAddon=void 0}render(){return c`
|
|
5
|
-
<section class="terminal-shell">
|
|
6
|
-
<div class="terminal-tabs">
|
|
7
|
-
${this.terminals.map(e=>c`
|
|
8
|
-
<button class=${this.selectedId===e.id?"selected":""} @click=${()=>{this.selectTerminal(e.id)}}>
|
|
9
|
-
<span>${e.name}${e.exited?" · exited":""}</span>
|
|
10
|
-
<small @click=${t=>{this.closeTerminal(e.id,t)}}>×</small>
|
|
11
|
-
</button>
|
|
12
|
-
`)}
|
|
13
|
-
<button class="new" ?disabled=${this.workspace===void 0} @click=${()=>{this.startTerminal()}}>+ Shell</button>
|
|
14
|
-
</div>
|
|
15
|
-
${this.error===void 0?null:c`<p class="error">${this.error}</p>`}
|
|
16
|
-
${this.loading?c`<p class="muted">Loading terminals…</p>`:null}
|
|
17
|
-
<div class="terminal-host"></div>
|
|
18
|
-
</section>
|
|
19
|
-
`}};o.styles=I`
|
|
20
|
-
:host { flex: 1 1 auto; min-height: 0; display: flex; }
|
|
21
|
-
.terminal-shell { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; overflow: hidden; background: var(--pi-terminal-bg); }
|
|
22
|
-
.terminal-tabs { flex: 0 0 auto; display: flex; gap: 6px; align-items: center; padding: 6px; border-bottom: 1px solid var(--pi-border-muted); background: var(--pi-bg); overflow: auto; }
|
|
23
|
-
button { display: inline-flex; align-items: center; gap: 6px; min-width: 0; max-width: 180px; border: 1px solid var(--pi-border); border-radius: 7px; background: var(--pi-surface); color: var(--pi-text); padding: 5px 7px; cursor: pointer; }
|
|
24
|
-
button.selected { border-color: var(--pi-accent); background: var(--pi-selection-bg); }
|
|
25
|
-
button.new { flex: 0 0 auto; color: var(--pi-muted); }
|
|
26
|
-
button span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
27
|
-
button small { color: var(--pi-muted); font-size: 14px; line-height: 1; }
|
|
28
|
-
button small:hover { color: var(--pi-danger); }
|
|
29
|
-
button:disabled { opacity: .5; cursor: not-allowed; }
|
|
30
|
-
.terminal-host { flex: 1 1 auto; min-height: 0; padding: 6px; box-sizing: border-box; overflow: hidden; }
|
|
31
|
-
.terminal-host .xterm { height: 100%; cursor: text; position: relative; user-select: none; }
|
|
32
|
-
.terminal-host .xterm.focus, .terminal-host .xterm:focus { outline: none; }
|
|
33
|
-
.terminal-host .xterm-helpers { position: absolute; top: 0; z-index: 5; }
|
|
34
|
-
.terminal-host .xterm-helper-textarea { position: absolute !important; left: -9999em !important; top: 0 !important; width: 0 !important; height: 0 !important; min-width: 0 !important; min-height: 0 !important; padding: 0 !important; border: 0 !important; margin: 0 !important; opacity: 0 !important; z-index: -5 !important; white-space: nowrap !important; overflow: hidden !important; resize: none !important; outline: 0 !important; appearance: none !important; }
|
|
35
|
-
.terminal-host .xterm-viewport { position: absolute; inset: 0; overflow-y: scroll; cursor: default; background-color: var(--pi-terminal-bg); }
|
|
36
|
-
.terminal-host .xterm-screen { position: relative; }
|
|
37
|
-
.terminal-host .xterm-screen canvas { position: absolute; left: 0; top: 0; }
|
|
38
|
-
.terminal-host .xterm-char-measure-element { display: inline-block; visibility: hidden; position: absolute; top: 0; left: -9999em; line-height: normal; }
|
|
39
|
-
.terminal-host .xterm-accessibility:not(.debug), .terminal-host .xterm-message { position: absolute; inset: 0; z-index: 10; color: transparent; pointer-events: none; }
|
|
40
|
-
.terminal-host .xterm-accessibility-tree:not(.debug) *::selection { color: transparent; }
|
|
41
|
-
.terminal-host .xterm-accessibility-tree { font-family: monospace; user-select: text; white-space: pre; }
|
|
42
|
-
.terminal-host .xterm-accessibility-tree > div { transform-origin: left; width: fit-content; }
|
|
43
|
-
.terminal-host .live-region { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
|
|
44
|
-
.error { flex: 0 0 auto; margin: 0; padding: 8px; color: var(--pi-danger); border-bottom: 1px solid var(--pi-border); background: var(--pi-surface); }
|
|
45
|
-
.muted { margin: 10px; color: var(--pi-muted); }
|
|
46
|
-
.xterm { height: 100%; }
|
|
47
|
-
`;a([p({attribute:!1})],o.prototype,"workspace",2);a([p({attribute:!1})],o.prototype,"selectedTerminalId",2);a([p({type:Boolean})],o.prototype,"autoStart",2);a([p({attribute:!1})],o.prototype,"onSelectTerminal",2);a([k(".terminal-host")],o.prototype,"terminalHost",2);a([d()],o.prototype,"terminals",2);a([d()],o.prototype,"selectedId",2);a([d()],o.prototype,"loading",2);a([d()],o.prototype,"error",2);a([d()],o.prototype,"visible",2);o=a([S("terminal-panel")],o);function E(e){const t=JSON.parse(e);if(!D(t))return{type:"error",message:"Invalid terminal message"};const i=t;return i.type==="output"&&typeof i.data=="string"?{type:"output",data:i.data,...typeof i.replay=="boolean"?{replay:i.replay}:{}}:i.type==="exit"?{type:"exit",...typeof i.exitCode=="number"?{exitCode:i.exitCode}:{}}:i.type==="error"&&typeof i.message=="string"?{type:"error",message:i.message}:{type:"error",message:"Invalid terminal message"}}function $(e){return e.replaceAll("\x1B[I","").replaceAll("\x1B[O","")}async function V(e){return typeof e=="string"?e:e instanceof ArrayBuffer?new TextDecoder().decode(e):e instanceof Blob?await e.text():String(e)}function b(e){return{...A,theme:g(e)}}function g(e){return{background:m(e,"--pi-terminal-bg","#05070a"),foreground:m(e,"--pi-terminal-text","#e6edf3"),cursor:m(e,"--pi-accent","#58a6ff"),selectionBackground:m(e,"--pi-terminal-selection","#264f78")}}function m(e,t,i){const r=getComputedStyle(e).getPropertyValue(t).trim();return r===""?i:r}function w(e){if(!(e===void 0||!P(e.cols,e.rows)))return{cols:Math.floor(e.cols),rows:Math.floor(e.rows)}}function P(e,t){return Number.isFinite(e)&&Number.isFinite(t)&&e>0&&t>0}function D(e){return typeof e=="object"&&e!==null}export{o as TerminalPanel,$ as filterTerminalInput};
|