Archived
762 lines
25 KiB
Markdown
762 lines
25 KiB
Markdown
# PI WEB plugin API
|
|
|
|
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.
|
|
|
|
Plugins can currently:
|
|
|
|
- add action-palette commands;
|
|
- add workspace tools/panels next to Files, Git, and Terminal;
|
|
- add compact workspace-label items in the workspace list, panel header, and status bar;
|
|
- call browser APIs and documented PI WEB plugin context helpers;
|
|
- read workspace files and start workspace terminal commands through documented helpers;
|
|
- serve their own static assets from the plugin directory.
|
|
|
|
They do **not** run in the session daemon, do not get a server-side hook API, and are not sandboxed.
|
|
|
|
## Trust model
|
|
|
|
Plugins run as JavaScript in the browser app. Treat them as trusted code:
|
|
|
|
- they can call browser APIs;
|
|
- they can read workspace files and start terminal commands through documented plugin helpers;
|
|
- they can render arbitrary Lit templates/custom elements in plugin contribution areas;
|
|
- they should not be installed from untrusted sources.
|
|
|
|
PI WEB's `/api/...` HTTP and WebSocket endpoints are internal implementation details. Plugin code should not fetch PI WEB API endpoints directly; use the documented context helpers instead.
|
|
|
|
## What to ask AI to build
|
|
|
|
Humans should not need to hand-code plugins. Give an AI agent a concrete UI goal and ask it to create or modify a local plugin.
|
|
|
|
Good plugin requests:
|
|
|
|
- "Show a workspace badge with the dev server URL from `.env`."
|
|
- "Add a workspace panel with links to logs, dashboards, and local services for this repo."
|
|
- "Add an action-palette command that starts a standard code-review prompt."
|
|
- "Show whether the current workspace is a git worktree, main checkout, staging env, or feature branch."
|
|
- "Add a compact status badge based on a project health file or command output saved in the repo."
|
|
|
|
Copy-paste prompt for creating a plugin:
|
|
|
|
```text
|
|
Build a PI WEB plugin for this project.
|
|
Goal: <describe the UI behavior>.
|
|
Before coding, read the PI WEB plugin docs:
|
|
https://pi-web.dev/plugins.html
|
|
Full API reference:
|
|
https://pi-web.dev/plugins.md
|
|
Create it as a local plugin under ~/.pi-web/plugins/<plugin-id>.
|
|
Use the appropriate extension points from the docs.
|
|
Validate by checking /pi-web-plugins/manifest.json and explain how to reload/debug it.
|
|
Do not modify PI WEB itself.
|
|
```
|
|
|
|
Copy-paste prompt for modifying a plugin:
|
|
|
|
```text
|
|
Improve the PI WEB plugin at <path>.
|
|
Before coding, read the PI WEB plugin docs:
|
|
https://pi-web.dev/plugins.html
|
|
Full API reference:
|
|
https://pi-web.dev/plugins.md
|
|
Keep the plugin compatible with the documented v1 API.
|
|
After editing, check the manifest endpoint and browser-console failure cases.
|
|
```
|
|
|
|
## Canonical example: bundled Info plugin
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
Source files:
|
|
|
|
```text
|
|
pi-web-plugins/info/package.json
|
|
pi-web-plugins/info/pi-web-plugin.ts
|
|
```
|
|
|
|
Built module:
|
|
|
|
```text
|
|
dist/pi-web-plugins/info/pi-web-plugin.js
|
|
```
|
|
|
|
Package metadata:
|
|
|
|
```json
|
|
{
|
|
"name": "@pi-web/info-plugin",
|
|
"private": true,
|
|
"piWeb": {
|
|
"plugins": [
|
|
{ "id": "info", "module": "pi-web-plugin.js" }
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
Module shape excerpt:
|
|
|
|
```js
|
|
export default {
|
|
apiVersion: 1,
|
|
name: "Info Plugin",
|
|
activate: ({ html, svg }) => ({
|
|
contributions: {
|
|
actions: [/* action definitions */],
|
|
workspaceLabels: [/* compact label definitions */],
|
|
workspacePanels: [/* panel definitions using html, optional icons using svg */],
|
|
},
|
|
}),
|
|
};
|
|
```
|
|
|
|
When copying the Info plugin, choose a new plugin id so it does not conflict with the bundled `info` plugin.
|
|
|
|
PI WEB also ships an `updates` plugin that demonstrates dynamic `visible` and `badge` callbacks for tabs that only appear when the host has status messages or needs extra install visibility.
|
|
|
|
## Local plugin usage
|
|
|
|
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.
|
|
|
|
Symlink a plugin folder into PI WEB's local plugin directory:
|
|
|
|
```bash
|
|
mkdir -p ~/.pi-web/plugins
|
|
ln -s /path/to/plugin-folder ~/.pi-web/plugins/plugin-id
|
|
```
|
|
|
|
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.
|
|
|
|
## Manage plugins
|
|
|
|
Open **Settings → Plugins** to review discovered bundled, local, dev, and Pi package plugins. PI WEB can disable any discovered plugin before the browser imports it. Core app contributions such as the built-in command palette, base workspace tools, and themes are not managed through this plugin list.
|
|
|
|
Plugin preferences are stored under the top-level `plugins` config key in the PI WEB config file:
|
|
|
|
```json
|
|
{
|
|
"plugins": {
|
|
"workspace-tasks": {
|
|
"enabled": true,
|
|
"settings": {}
|
|
},
|
|
"info": {
|
|
"enabled": false
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Plugins are enabled by default. Set `enabled` to `false` to remove a plugin from `/pi-web-plugins/manifest.json` so the browser will not import or activate it on the next page load. The optional `settings` object is reserved for plugin-specific settings.
|
|
|
|
After changing plugin enablement, reload the PI WEB browser tab. Already-loaded plugin JavaScript is not unloaded from the current page.
|
|
|
|
## Built-in plugins
|
|
|
|
PI WEB ships core, discoverable plugins in the main `@jmfederico/pi-web` npm package. No separate `pi install` step is required: update PI WEB, reload the browser tab, and the bundled plugins appear in `/pi-web-plugins/manifest.json`.
|
|
|
|
Built-in plugins can be managed from **Settings → Plugins** or with the top-level `plugins` config key.
|
|
|
|
### Updates
|
|
|
|
**Plugin id:** `updates`
|
|
**What it does:** adds a conditional **Updates** workspace tab with PI WEB update, restart, and installed-service guidance.
|
|
|
|
Updates is enabled by default. To hide it, disable `updates` in **Settings → Plugins** or set:
|
|
|
|
```json
|
|
{
|
|
"plugins": {
|
|
"updates": { "enabled": false }
|
|
}
|
|
}
|
|
```
|
|
|
|
### Workspace Tasks
|
|
|
|
**Plugin id:** `workspace-tasks`
|
|
**Config file:** `.pi-web/tasks.json`
|
|
**What it does:** adds a **Tasks** workspace tab for running configured shell commands in dedicated PI WEB terminals.
|
|
|
|
Workspace Tasks is enabled by default. To hide it, disable `workspace-tasks` in **Settings → Plugins** or set:
|
|
|
|
```json
|
|
{
|
|
"plugins": {
|
|
"workspace-tasks": { "enabled": false }
|
|
}
|
|
}
|
|
```
|
|
|
|
Configure workspace tasks in `.pi-web/tasks.json`:
|
|
|
|
```json
|
|
{
|
|
"version": 1,
|
|
"tasks": [
|
|
{
|
|
"id": "docker.start",
|
|
"title": "Start Docker",
|
|
"group": "Docker",
|
|
"description": "Start the local Docker Compose environment.",
|
|
"command": "./docker/scripts/docker-compose-dev up -d"
|
|
},
|
|
{
|
|
"id": "db.reset",
|
|
"title": "Reset DB",
|
|
"group": "Database",
|
|
"command": "go -C klingit-go run ./cli db reset",
|
|
"confirm": true
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Open a workspace, choose the **Tasks** tab, and click **Run** next to a task. Commands run in the workspace root because PI WEB creates the terminal for that workspace.
|
|
|
|
Task fields:
|
|
|
|
- `version`: must be `1`.
|
|
- `tasks`: array of task definitions.
|
|
- `id`: stable task id, matching `^[a-z][a-z0-9.-]*$`.
|
|
- `title`: button label.
|
|
- `command`: literal shell command sent to the terminal.
|
|
- `description`: optional explanatory text.
|
|
- `group`: optional group heading.
|
|
- `confirm`: optional boolean. When true, the browser asks before dispatching the command.
|
|
|
|
Review task configs before running them, especially in shared projects. Workspace Tasks runs trusted shell commands from your repositories.
|
|
|
|
## Discovery and packaging
|
|
|
|
PI WEB builds `/pi-web-plugins/manifest.json` from these sources:
|
|
|
|
1. Bundled plugins in the PI WEB package:
|
|
|
|
```text
|
|
pi-web-plugins/<plugin-package>/
|
|
```
|
|
|
|
2. User-local plugins:
|
|
|
|
```text
|
|
~/.pi-web/plugins/<plugin-package>/
|
|
```
|
|
|
|
Entries may be real directories or symlinks. This is the recommended development workflow.
|
|
|
|
3. Installed Pi packages that expose PI WEB plugin metadata. Pi packages may be user or project scoped.
|
|
|
|
Plugin package directory names and plugin ids must be valid identifiers:
|
|
|
|
```text
|
|
^[a-z][a-z0-9.-]*$
|
|
```
|
|
|
|
A package can expose one or more PI WEB plugin modules. There is exactly one supported `package.json` metadata shape:
|
|
|
|
```json
|
|
{
|
|
"private": true,
|
|
"piWeb": {
|
|
"plugins": [
|
|
{ "id": "review", "module": "dist/review.js" },
|
|
{ "id": "dashboard", "module": "dist/dashboard.js" }
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
Rules:
|
|
|
|
- `piWeb.plugins` must be an array of objects.
|
|
- Each entry must have an explicit `id` and `module`.
|
|
- `id` must match `^[a-z][a-z0-9.-]*$`.
|
|
- `module` must be a safe relative path inside the plugin package root.
|
|
- Duplicate plugin ids are not auto-renamed; later duplicates are skipped.
|
|
- Legacy shortcuts such as `piWeb.plugin`, string entries in `piWeb.plugins`, `piWeb.id` fallback ids, and no-`package.json` fallbacks are not supported.
|
|
|
|
### Manifest and assets
|
|
|
|
The manifest contains each discovered plugin module:
|
|
|
|
```json
|
|
{
|
|
"plugins": [
|
|
{
|
|
"id": "my-plugin",
|
|
"module": "/pi-web-plugins/my-plugin/pi-web-plugin.js?v=1234567890",
|
|
"source": "local",
|
|
"scope": "local"
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
`source` describes where the plugin came from (`bundled`, `local`, or the Pi package source). `scope` is `bundled`, `local`, `user`, or `project`.
|
|
|
|
A plugin can fetch its own static assets with URLs under:
|
|
|
|
```text
|
|
/pi-web-plugins/<plugin-id>/<path-inside-plugin-root>
|
|
```
|
|
|
|
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.
|
|
|
|
## Plugin module shape
|
|
|
|
The entry module must default-export a plugin object:
|
|
|
|
```ts
|
|
interface PiWebPlugin {
|
|
apiVersion: 1;
|
|
name: string;
|
|
activate: (context: PluginActivationContext) => PluginActivationResult;
|
|
}
|
|
|
|
interface PluginActivationContext {
|
|
apiVersion: 1;
|
|
pluginId: string;
|
|
html: typeof import("lit").html;
|
|
svg: typeof import("lit").svg;
|
|
}
|
|
|
|
interface PluginActivationResult {
|
|
contributions: PluginContributions;
|
|
}
|
|
```
|
|
|
|
Example:
|
|
|
|
```js
|
|
export default {
|
|
apiVersion: 1,
|
|
name: "My Plugin",
|
|
activate: ({ pluginId, html }) => ({
|
|
contributions: {
|
|
actions: [],
|
|
workspacePanels: [],
|
|
workspaceLabels: [],
|
|
},
|
|
}),
|
|
};
|
|
```
|
|
|
|
`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.
|
|
|
|
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:
|
|
|
|
```text
|
|
<plugin-id>:<local-contribution-id>
|
|
```
|
|
|
|
For example, plugin `info` with action `workspace.show-path` becomes `info:workspace.show-path`.
|
|
|
|
## Contributions
|
|
|
|
`activate()` returns a `contributions` object with any combination of these arrays:
|
|
|
|
```ts
|
|
interface PluginContributions {
|
|
actions?: PluginAction[];
|
|
workspacePanels?: WorkspacePanelContribution[];
|
|
workspaceLabels?: WorkspaceLabelContribution[];
|
|
}
|
|
```
|
|
|
|
### Actions
|
|
|
|
Actions appear in the action palette. They can inspect app state and call UI/runtime helpers.
|
|
|
|
```js
|
|
actions: [
|
|
{
|
|
id: "workspace.show-path",
|
|
title: "Show Current Workspace Path",
|
|
description: "Display the selected workspace path",
|
|
shortcut: "mod+shift+p",
|
|
group: "Info",
|
|
enabled: (context) => context.state.selectedWorkspace !== undefined,
|
|
run: (context) => {
|
|
window.alert(context.state.selectedWorkspace?.path ?? "No workspace selected");
|
|
},
|
|
},
|
|
]
|
|
```
|
|
|
|
Action type:
|
|
|
|
```ts
|
|
interface PluginAction {
|
|
id: string;
|
|
title: string;
|
|
description?: string;
|
|
shortcut?: string;
|
|
group?: string;
|
|
enabled?: (context: PluginRuntimeContext) => boolean;
|
|
run: (context: PluginRuntimeContext) => void | Promise<void>;
|
|
}
|
|
```
|
|
|
|
Stable runtime context fields:
|
|
|
|
```ts
|
|
interface PluginRuntimeContext {
|
|
state: {
|
|
selectedWorkspace?: Workspace;
|
|
selectedSession?: unknown;
|
|
piWebStatus?: PiWebStatusResponse;
|
|
};
|
|
openActionPalette: () => void;
|
|
focusPrompt: () => void;
|
|
addProject: () => void | Promise<void>;
|
|
configureAuth: () => void | Promise<void>;
|
|
logoutAuth: () => void | Promise<void>;
|
|
selectWorkspaceTool: (tool: QualifiedContributionId) => void;
|
|
openTerminal: (options?: { terminalId?: string }) => void;
|
|
refreshFiles: () => void | Promise<void>;
|
|
refreshGit: () => void | Promise<void>;
|
|
startSession: () => void | Promise<void>;
|
|
archiveSession: () => void | Promise<void>;
|
|
stopActiveWork: () => void | Promise<void>;
|
|
}
|
|
```
|
|
|
|
Notes:
|
|
|
|
- `state` is a snapshot of current UI state when actions are built.
|
|
- The stable state fields are `state.selectedWorkspace`, `state.selectedSession`, and `state.piWebStatus`.
|
|
- Other `state` fields may exist at runtime, but they are PI WEB internals and can change quickly.
|
|
- `enabled` is evaluated when the action palette asks for actions.
|
|
- `selectWorkspaceTool()` expects a qualified panel id such as `my-plugin:workspace.info`.
|
|
- `openTerminal()` switches to the built-in terminal panel. Pass `{ terminalId }` to deep-link to a specific terminal.
|
|
- Only fields documented here and declared in `plugin-api.d.ts` are stable public plugin API. Unstable runtime fields are intentionally omitted from these types; if a plugin author chooses to depend on them, they must explicitly import unstable types from `@jmfederico/pi-web/plugin-api/unstable` and type-assert the context in their own code.
|
|
|
|
#### Keyboard shortcuts
|
|
|
|
- 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.
|
|
- `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.
|
|
- Use modified shortcuts such as `mod+shift+p`; plain letter shortcuts are intentionally ignored so normal typing is never captured.
|
|
- 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.
|
|
- Choose shortcuts carefully to avoid conflicts. There is no user-facing shortcut override or conflict resolver yet.
|
|
- 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.
|
|
|
|
### Workspace panels
|
|
|
|
Workspace panels add tools next to built-in workspace tools. They render inside the workspace side panel on desktop and as mobile tabs on smaller screens.
|
|
|
|
```js
|
|
workspacePanels: [
|
|
{
|
|
id: "workspace.info",
|
|
title: "Info",
|
|
icon: svg`
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<circle cx="12" cy="12" r="9"></circle>
|
|
<path d="M12 10v6"></path>
|
|
<path d="M12 7h.01"></path>
|
|
</svg>
|
|
`,
|
|
order: 100,
|
|
visible: ({ workspace }) => workspace.isGitRepo,
|
|
render: ({ workspace }) => html`
|
|
<section class="toolbar"><strong>Info</strong></section>
|
|
<section class="viewer">
|
|
<p class="muted">${workspace.label}</p>
|
|
<p class="muted">${workspace.path}</p>
|
|
</section>
|
|
`,
|
|
},
|
|
]
|
|
```
|
|
|
|
Panel type:
|
|
|
|
```ts
|
|
interface WorkspacePanelContribution {
|
|
id: string;
|
|
title: string;
|
|
icon?: TemplateResult;
|
|
order?: number;
|
|
visible?: (context: WorkspacePanelContext) => boolean;
|
|
badge?: (context: WorkspacePanelContext) => string | number | TemplateResult | undefined;
|
|
render: (context: WorkspacePanelContext) => TemplateResult;
|
|
}
|
|
|
|
interface WorkspacePanelContext {
|
|
machine: PluginMachine;
|
|
workspace: Workspace;
|
|
state?: PluginRuntimeState;
|
|
files: {
|
|
readFile(path: string): Promise<FileContentResponse>;
|
|
};
|
|
terminal: {
|
|
open(options?: { terminalId?: string }): void;
|
|
runCommand(input: {
|
|
title: string;
|
|
command: string;
|
|
metadata?: Record<string, string>;
|
|
open?: boolean;
|
|
}): Promise<TerminalCommandRunHandle>;
|
|
};
|
|
requestRender: () => void;
|
|
openTerminal: (options?: { terminalId?: string }) => void;
|
|
}
|
|
```
|
|
|
|
`icon` is optional and is used in the compact mobile tab bar. Prefer an SVG rendered with the `svg` helper from `PluginActivationContext`; use `currentColor` so PI WEB themes can style it. If `icon` is omitted, mobile tabs fall back to initials from the panel title, or to the full title when initials collide.
|
|
|
|
`machine`, `workspace`, `files`, `terminal`, `requestRender()`, and `openTerminal()` are documented as stable for panel callbacks. `terminal.open()` is equivalent to `openTerminal()`; new plugins should prefer `terminal.open()` so terminal-related helpers live under one capability.
|
|
|
|
Useful workspace and machine shapes:
|
|
|
|
```ts
|
|
interface PluginMachine {
|
|
id: string;
|
|
name: string;
|
|
kind: "local" | "remote";
|
|
}
|
|
|
|
interface Workspace {
|
|
id: string;
|
|
projectId: string;
|
|
path: string;
|
|
label: string;
|
|
branch?: string;
|
|
isMain: boolean;
|
|
isGitRepo: boolean;
|
|
isGitWorktree: boolean;
|
|
}
|
|
```
|
|
|
|
`machine.id` is included in panel contexts so plugins can keep caches machine-scoped. Do not infer the selected machine from global browser state.
|
|
|
|
Use existing classes such as `toolbar`, `viewer`, `empty`, and `muted` for panel content when possible. Do not assume a panel owns the whole page; keep layout contained.
|
|
|
|
### Workspace labels
|
|
|
|
Workspace labels add compact inline metadata wherever PI WEB displays a workspace label: workspace list, workspace panel header, and status bar.
|
|
|
|
Use them for short facts like project environment, local URL, branch status, container name, or health state.
|
|
|
|
```js
|
|
workspaceLabels: [
|
|
{
|
|
id: "dev-url",
|
|
order: 10,
|
|
visible: ({ workspace }) => workspace.path.includes("my-app"),
|
|
items: () => [{
|
|
type: "link",
|
|
text: "web:5173",
|
|
href: "http://localhost:5173",
|
|
title: "Open dev server",
|
|
target: "_blank",
|
|
}],
|
|
},
|
|
]
|
|
```
|
|
|
|
Label contribution type:
|
|
|
|
```ts
|
|
interface WorkspaceLabelContribution {
|
|
id: string;
|
|
order?: number;
|
|
visible?: (context: WorkspaceLabelContext) => boolean;
|
|
items: (context: WorkspaceLabelContext) => WorkspaceLabelItem[];
|
|
}
|
|
|
|
interface WorkspaceLabelContext {
|
|
machine: PluginMachine;
|
|
workspace: Workspace;
|
|
state?: PluginRuntimeState;
|
|
}
|
|
```
|
|
|
|
`machine` and `workspace` are documented as stable for label callbacks. Include `machine.id` in any label caches that depend on workspace data.
|
|
|
|
Items are sorted by `order` and then id. Return an empty array to render nothing.
|
|
|
|
#### Text items
|
|
|
|
```js
|
|
{ type: "text", text: "staging", title: "Staging workspace" }
|
|
```
|
|
|
|
#### Link items
|
|
|
|
```js
|
|
{
|
|
type: "link",
|
|
text: "web:5173",
|
|
href: "http://localhost:5173",
|
|
title: "Open dev server",
|
|
target: "_blank"
|
|
}
|
|
```
|
|
|
|
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.
|
|
|
|
#### Render items
|
|
|
|
Use render items when a label contribution needs custom UI, async data, or caching. Render items should stay compact and inline.
|
|
|
|
```js
|
|
class MyWorkspaceBadge extends HTMLElement {
|
|
set workspace(value) {
|
|
this._workspace = value;
|
|
this.textContent = value?.branch === "main" ? "main" : "branch";
|
|
}
|
|
}
|
|
|
|
if (!customElements.get("my-workspace-badge")) {
|
|
customElements.define("my-workspace-badge", MyWorkspaceBadge);
|
|
}
|
|
|
|
export default {
|
|
apiVersion: 1,
|
|
name: "My Plugin",
|
|
activate: ({ html }) => ({
|
|
contributions: {
|
|
workspaceLabels: [
|
|
{
|
|
id: "badge",
|
|
order: 10,
|
|
items: ({ workspace }) => [{
|
|
type: "render",
|
|
render: () => html`<my-workspace-badge .workspace=${workspace}></my-workspace-badge>`,
|
|
}],
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
};
|
|
```
|
|
|
|
## Reading workspace files
|
|
|
|
Workspace panels can read files through the documented `files` helper. PI WEB binds this helper to the panel's machine and workspace, so it works the same for local and federated machines.
|
|
|
|
```js
|
|
workspacePanels: [
|
|
{
|
|
id: "workspace.env",
|
|
title: "Env",
|
|
render: ({ files, requestRender }) => html`
|
|
<my-env-viewer .files=${files} .requestRender=${requestRender}></my-env-viewer>
|
|
`,
|
|
},
|
|
]
|
|
|
|
class MyEnvViewer extends HTMLElement {
|
|
set files(value) {
|
|
this._files = value;
|
|
void this.load();
|
|
}
|
|
|
|
async load() {
|
|
try {
|
|
const file = await this._files.readFile(".env.example");
|
|
this.textContent = file.binary ? "Binary file" : file.content;
|
|
} catch (error) {
|
|
this.textContent = error instanceof Error ? error.message : String(error);
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
The file response includes fields such as `path`, `content`, `truncated`, and `binary`. Be careful with sensitive files such as `.env`: plugins are trusted browser code, and file contents are exposed to the plugin.
|
|
|
|
## Running workspace terminal commands
|
|
|
|
Workspace panels can start terminal commands through the documented `terminal` helper. Commands run in the current workspace on the panel's machine.
|
|
|
|
```js
|
|
render: ({ terminal }) => html`
|
|
<button @click=${() => terminal.runCommand({
|
|
title: "Build",
|
|
command: "npm run build",
|
|
open: true,
|
|
metadata: { "my-plugin.task": "build" },
|
|
})}>Build</button>
|
|
`
|
|
```
|
|
|
|
Review command strings carefully. They are trusted shell commands executed in the workspace terminal.
|
|
|
|
## Internal PI WEB APIs and explicit unstable opt-in
|
|
|
|
PI WEB's `/api/...` HTTP and WebSocket routes are private implementation details. Plugin code should not fetch PI WEB API endpoints directly because those URLs, response shapes, and machine-federation routing rules can change.
|
|
|
|
If a plugin author deliberately chooses to depend on an unstable runtime field while a public helper is still being designed, make that decision explicit in code with a type-only unstable import and a local type assertion:
|
|
|
|
```ts
|
|
import type { WorkspacePanelContext } from "@jmfederico/pi-web/plugin-api";
|
|
import type { UnstableWorkspacePanelContext } from "@jmfederico/pi-web/plugin-api/unstable";
|
|
|
|
function unstableContext(context: WorkspacePanelContext) {
|
|
return context as WorkspacePanelContext & UnstableWorkspacePanelContext;
|
|
}
|
|
```
|
|
|
|
Unstable APIs are not covered by the v1 compatibility promise. Prefer documented helpers whenever they exist.
|
|
|
|
## Async data and caching
|
|
|
|
PI WEB does not provide a plugin cache/invalidation framework. Keep host callbacks cheap:
|
|
|
|
- simple contributions should be synchronous and cheap;
|
|
- expensive or async work should live inside the plugin;
|
|
- custom elements in `type: "render"` label items or panels are a good place to own async loading;
|
|
- dedupe async reads/commands and avoid unbounded polling;
|
|
- clean up intervals/event listeners in custom elements' `disconnectedCallback()`.
|
|
|
|
## Agent implementation checklist
|
|
|
|
If you are an AI agent building or editing a PI WEB plugin, follow this checklist:
|
|
|
|
1. Create or update a plugin folder with `package.json` and a JavaScript module such as `pi-web-plugin.js`.
|
|
2. Use the single supported package metadata shape: `piWeb.plugins` array with `{ id, module }` entries.
|
|
3. Default-export `{ apiVersion: 1, name, activate }` from the module.
|
|
4. Return `{ contributions: { actions, workspacePanels, workspaceLabels } }` from `activate()`.
|
|
5. Use ids matching `^[a-z][a-z0-9.-]*$`.
|
|
6. Use the activation context's `html` function for Lit templates.
|
|
7. Keep `activate()` synchronous and cheap; return contribution definitions only.
|
|
8. Add actions for command-palette operations.
|
|
9. Add workspace panels for larger workspace UI.
|
|
10. Add workspace labels for compact inline metadata.
|
|
11. Return arrays from workspace label `items()`; return an empty array to render nothing.
|
|
12. Use documented context helpers first: `files`, `terminal`, `requestRender`, `workspace`, `machine`, `state.selectedWorkspace`, `state.selectedSession`, and `state.piWebStatus`.
|
|
13. Do not fetch PI WEB `/api/...` endpoints directly. If an unstable runtime field is intentionally required, import the type from `@jmfederico/pi-web/plugin-api/unstable` and type-assert locally.
|
|
14. Treat plugins as trusted code and avoid reading or displaying secrets unless intentional.
|
|
15. After local edits, tell the user to hard reload the browser and check the console for plugin errors.
|
|
|
|
## Troubleshooting
|
|
|
|
Check discovery:
|
|
|
|
```bash
|
|
curl http://127.0.0.1:8504/pi-web-plugins/manifest.json
|
|
```
|
|
|
|
Check a plugin module:
|
|
|
|
```bash
|
|
curl http://127.0.0.1:8504/pi-web-plugins/my-plugin/pi-web-plugin.js
|
|
```
|
|
|
|
Common issues:
|
|
|
|
- invalid plugin id or contribution id;
|
|
- missing default export;
|
|
- missing `apiVersion: 1`, `name`, or `activate` function;
|
|
- missing `package.json` or incorrect `piWeb.plugins` metadata;
|
|
- legacy shortcuts such as `piWeb.plugin`, string plugin entries, or no-`package.json` fallback;
|
|
- duplicate plugin ids; later duplicates are skipped rather than renamed;
|
|
- entry module path points outside the plugin root or file does not exist;
|
|
- browser cache not refreshed after editing;
|
|
- plugin directory is not under `~/.pi-web/plugins` or symlinked there;
|
|
- plugin throws during module import, `activate()`, `visible()`, `enabled()`, `items()`, or `render()`; check the browser console.
|