From 8517800a24f364dd1c3ecf474f15257ebbb28720 Mon Sep 17 00:00:00 2001 From: Federico Jaramillo Martinez Date: Mon, 27 Jul 2026 18:03:25 +0200 Subject: [PATCH] feat(plugins): turn Info panel into a PI WEB status view Rework the bundled Info plugin from a demo into an always-available status view: running and installed versions, installation details, release state, and per-service health rendered from host-provided state, plus machine and workspace details. Replace the window.alert demo action with a Copy PI WEB Diagnostics action that copies a plain-text summary for bug reports. Add state.selectedMachine to the stable plugin runtime state so actions and other runtime callbacks can read the selected machine's identity, and split the plugin into a copy-paste-friendly skeleton (pi-web-plugin.ts) and replaceable internals (infoInternals.ts). --- .changeset/info-plugin-status-view.md | 5 + .changeset/plugin-api-selected-machine.md | 5 + docs/plugins.html | 6 +- docs/plugins.md | 23 ++- pi-web-plugins/info/infoInternals.test.ts | 162 ++++++++++++++++ pi-web-plugins/info/infoInternals.ts | 219 ++++++++++++++++++++++ pi-web-plugins/info/pi-web-plugin.test.ts | 71 +++++++ pi-web-plugins/info/pi-web-plugin.ts | 28 ++- src/plugin-api.ts | 2 + 9 files changed, 496 insertions(+), 25 deletions(-) create mode 100644 .changeset/info-plugin-status-view.md create mode 100644 .changeset/plugin-api-selected-machine.md create mode 100644 pi-web-plugins/info/infoInternals.test.ts create mode 100644 pi-web-plugins/info/infoInternals.ts create mode 100644 pi-web-plugins/info/pi-web-plugin.test.ts diff --git a/.changeset/info-plugin-status-view.md b/.changeset/info-plugin-status-view.md new file mode 100644 index 0000000..d2656e5 --- /dev/null +++ b/.changeset/info-plugin-status-view.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Turn the bundled Info plugin panel into an always-available PI WEB status view: it now shows the running and installed versions, installation kind and path, release state, per-service health, and machine and workspace details from host-provided status, plus a "Copy PI WEB Diagnostics" action that copies a plain-text summary for bug reports. diff --git a/.changeset/plugin-api-selected-machine.md b/.changeset/plugin-api-selected-machine.md new file mode 100644 index 0000000..83be73c --- /dev/null +++ b/.changeset/plugin-api-selected-machine.md @@ -0,0 +1,5 @@ +--- +"@jmfederico/pi-web": patch +--- + +Add `state.selectedMachine` to the stable plugin runtime state so plugin actions and other runtime callbacks can read the selected machine's identity, not just workspace panel contexts. diff --git a/docs/plugins.html b/docs/plugins.html index 955df12..2b58fec 100644 --- a/docs/plugins.html +++ b/docs/plugins.html @@ -231,13 +231,17 @@ After editing, check the manifest endpoint and browser-console failure cases.

Read it on GitHub: pi-web-plugins/info. If you copy it, choose a new plugin id so it does not conflict with the bundled info plugin. + Its panel also doubles as an always-available PI WEB status view: it renders the host-provided + context.state.piWebStatus without issuing its own requests, and its action copies a + plain-text diagnostics summary suitable for bug reports.

The bundled updates plugin demonstrates dynamic visible and badge diff --git a/docs/plugins.md b/docs/plugins.md index cb4a988..f58623d 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -90,8 +90,11 @@ Source files: ```text pi-web-plugins/info/package.json pi-web-plugins/info/pi-web-plugin.ts +pi-web-plugins/info/infoInternals.ts ``` +`pi-web-plugin.ts` is the plugin skeleton: metadata plus contribution definitions. `infoInternals.ts` holds everything the bundled panel and action actually render, so you can ignore or replace it when copying the plugin. + Built module: ```text @@ -130,6 +133,8 @@ export default { When copying the Info plugin, choose a new plugin id so it does not conflict with the bundled `info` plugin. +The Info panel doubles as an always-available PI WEB status view: it renders the host-provided `context.state.piWebStatus` (versions, installation, release state, machine, and workspace details) without issuing its own requests, and its action copies a plain-text diagnostics summary suitable for bug reports. + 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 @@ -433,14 +438,13 @@ Actions appear in the action palette. They can inspect app state and call UI/run ```js actions: [ { - id: "workspace.show-path", - title: "Show Current Workspace Path", - description: "Display the selected workspace path", - shortcut: "mod+shift+p", + id: "copy-diagnostics", + title: "Copy PI WEB Diagnostics", + description: "Copy version, installation, and status details for this machine", group: "Info", - enabled: (context) => context.state.selectedWorkspace !== undefined, - run: (context) => { - window.alert(context.state.selectedWorkspace?.path ?? "No workspace selected"); + run: async (context) => { + const version = context.state.piWebStatus?.components.web.runtimeVersion ?? "unknown"; + await navigator.clipboard.writeText(`PI WEB ${version}`); }, }, ] @@ -468,6 +472,7 @@ Stable runtime context fields: ```ts interface PluginRuntimeContext { state: { + selectedMachine?: PluginMachine; selectedWorkspace?: Workspace; selectedSession?: unknown; piWebStatus?: PiWebStatusResponse; @@ -492,7 +497,7 @@ interface PluginRuntimeContext { 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`. `state.piWebStatus` describes the currently selected machine's PI WEB runtime, or the gateway/local runtime when the local machine is selected. +- The stable state fields are `state.selectedMachine`, `state.selectedWorkspace`, `state.selectedSession`, and `state.piWebStatus`. `state.selectedMachine` identifies the currently selected machine. `state.piWebStatus` describes the currently selected machine's PI WEB runtime, or the gateway/local runtime when the local machine is selected. - Other `state` fields may exist at runtime, but they are private PI WEB internals that may graduate into stable helpers, change shape, or disappear. - `enabled` is evaluated when the action palette asks for actions. - `selectWorkspaceTool()` expects a qualified panel id such as `my-plugin:workspace.info`. @@ -957,7 +962,7 @@ If you are an AI agent building or editing a PI WEB plugin, follow this checklis 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`, `host.requestRender`, `workspace`, `machine`, `state.selectedWorkspace`, `state.selectedSession`, `state.piWebStatus`, and `prompt`. +12. Use documented context helpers first: `files`, `terminal`, `host.requestRender`, `workspace`, `machine`, `state.selectedMachine`, `state.selectedWorkspace`, `state.selectedSession`, `state.piWebStatus`, and `prompt`. 13. Do not fetch PI WEB `/api/...` endpoints directly unless you intentionally accept private API churn; prefer documented helpers. 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. diff --git a/pi-web-plugins/info/infoInternals.test.ts b/pi-web-plugins/info/infoInternals.test.ts new file mode 100644 index 0000000..27e09ea --- /dev/null +++ b/pi-web-plugins/info/infoInternals.test.ts @@ -0,0 +1,162 @@ +import { describe, expect, it } from "vitest"; +import type { PiWebComponentStatus, PiWebReleaseStatus, PiWebStatusResponse, PluginMachine, Workspace } from "@jmfederico/pi-web/plugin-api"; +import { componentDetails, componentHealth, diagnosticsSummary, formatVersion, installationLabel, machineKindLabel, releaseSummary, workspaceFlags } from "./infoInternals.js"; + +describe("componentHealth", () => { + it("reports current when the component is available and not stale", () => { + expect(componentHealth(componentStatus())).toBe("current"); + }); + + it("reports restart needed when the installed version is newer than the running one", () => { + expect(componentHealth(componentStatus({ stale: true }))).toBe("restart needed"); + }); + + it("reports unavailable when the component cannot be reached", () => { + expect(componentHealth(componentStatus({ available: false, stale: true }))).toBe("unavailable"); + }); +}); + +describe("releaseSummary", () => { + it("names the latest version when an update is available", () => { + expect(releaseSummary(release({ updateAvailable: true, latestVersion: "1.2.3" }))).toBe("Update available: 1.2.3"); + }); + + it("still reports an update when the latest version is unknown", () => { + expect(releaseSummary(release({ updateAvailable: true }))).toBe("Update available"); + }); + + it("surfaces a failed release check", () => { + expect(releaseSummary(release({ error: "registry unreachable" }))).toBe("Update check failed: registry unreachable"); + }); + + it("reports skipped checks distinctly from an up-to-date install", () => { + expect(releaseSummary(release({ skipped: true }))).toBe("Update check skipped"); + expect(releaseSummary(release())).toBe("Up to date"); + }); +}); + +describe("componentDetails", () => { + it("combines versions, health, and installation into one line", () => { + expect(componentDetails(componentStatus())).toBe("running 1.0.0 · installed 1.0.0 · current · global npm package · /usr/lib/node_modules"); + }); + + it("includes the component error when present", () => { + const details = componentDetails(componentStatus({ available: false, error: "connection refused" })); + expect(details).toContain("unavailable"); + expect(details).toContain("error: connection refused"); + }); +}); + +describe("diagnosticsSummary", () => { + it("renders a full status block for bug reports", () => { + const summary = diagnosticsSummary({ status: statusResponse(), machine: machineFixture(), workspace: workspaceFixture() }); + + expect(summary).toBe([ + "PI WEB diagnostics", + "Package: @jmfederico/pi-web", + "Web/UI: running 1.0.0 · installed 1.0.1 · restart needed · global npm package · /usr/lib/node_modules", + "Session daemon: running 1.0.0 · installed 1.0.0 · current · local checkout · /srv/dev/pi-web", + "Release: Update available: 1.1.0 (checked 2025-01-02T03:04:05Z)", + "Status generated: 2025-01-02T03:04:06Z", + "Machine: devbox (local machine)", + "Workspace: pi-web — /srv/dev/pi-web (branch main, git worktree, main workspace)", + ].join("\n")); + }); + + it("degrades gracefully when the status and workspace are unavailable", () => { + const summary = diagnosticsSummary({ status: undefined }); + + expect(summary).toBe([ + "PI WEB diagnostics", + "Status: unavailable", + "Workspace: none selected", + ].join("\n")); + }); +}); + +describe("small formatters", () => { + it("formats missing versions as unknown", () => { + expect(formatVersion(undefined)).toBe("unknown"); + expect(formatVersion("")).toBe("unknown"); + expect(formatVersion("1.0.0")).toBe("1.0.0"); + }); + + it("labels installations", () => { + expect(installationLabel(undefined)).toBe("installation unknown"); + expect(installationLabel({ kind: "pi-package", source: "Pi package", scope: "user" })).toBe("Pi package · user"); + expect(installationLabel({ kind: "docker", dockerMode: "dev" })).toBe("Docker development runtime"); + expect(installationLabel({ kind: "docker" })).toBe("Docker runtime"); + expect(installationLabel({ kind: "unknown" })).toBe("installation unknown"); + }); + + it("labels machine kinds", () => { + expect(machineKindLabel("local")).toBe("local machine"); + expect(machineKindLabel("remote")).toBe("remote machine"); + }); + + it("describes workspaces without git metadata", () => { + expect(workspaceFlags({ + id: "ws-1", + projectId: "proj-1", + path: "/srv/dev/plain", + label: "plain", + isMain: false, + isGitRepo: false, + isGitWorktree: false, + })).toEqual(["not a git repo"]); + }); +}); + +function componentStatus(patch: Partial = {}): PiWebComponentStatus { + return { + component: "web", + label: "Web/UI", + runtimeVersion: "1.0.0", + installedVersion: "1.0.0", + stale: false, + available: true, + installation: { kind: "npm-global", path: "/usr/lib/node_modules" }, + ...patch, + }; +} + +function release(patch: Partial = {}): PiWebReleaseStatus { + return { + packageName: "@jmfederico/pi-web", + updateAvailable: false, + checkedAt: "2025-01-02T03:04:05Z", + ...patch, + }; +} + +function statusResponse(): PiWebStatusResponse { + return { + packageName: "@jmfederico/pi-web", + generatedAt: "2025-01-02T03:04:06Z", + components: { + web: componentStatus({ installedVersion: "1.0.1", stale: true }), + sessiond: componentStatus({ component: "sessiond", label: "Session daemon", installation: { kind: "local", path: "/srv/dev/pi-web" } }), + }, + release: release({ updateAvailable: true, latestVersion: "1.1.0" }), + commands: {}, + messages: [], + }; +} + +function machineFixture(): PluginMachine { + return { id: "local", name: "devbox", kind: "local" }; +} + +function workspaceFixture(patch: Partial = {}): Workspace { + return { + id: "ws-1", + projectId: "proj-1", + path: "/srv/dev/pi-web", + label: "pi-web", + branch: "main", + isMain: true, + isGitRepo: true, + isGitWorktree: true, + ...patch, + }; +} diff --git a/pi-web-plugins/info/infoInternals.ts b/pi-web-plugins/info/infoInternals.ts new file mode 100644 index 0000000..094a1c2 --- /dev/null +++ b/pi-web-plugins/info/infoInternals.ts @@ -0,0 +1,219 @@ +// Implementation details of the bundled Info plugin. +// +// This file is NOT part of the plugin skeleton. If you copied the Info plugin +// as a starting point for your own plugin, replace everything here with your +// own content — the plugin contract (metadata and contribution definitions) +// lives in pi-web-plugin.ts. + +import type { TemplateResult } from "lit"; +import type { HtmlTemplateTag, MachineKind, PiWebComponentStatus, PiWebInstallationInfo, PiWebReleaseStatus, PiWebStatusResponse, PluginMachine, PluginRuntimeContext, Workspace, WorkspacePanelContext } from "@jmfederico/pi-web/plugin-api"; + +export type ComponentHealth = "current" | "restart needed" | "unavailable"; + +export function componentHealth(component: PiWebComponentStatus): ComponentHealth { + if (!component.available) return "unavailable"; + if (component.stale) return "restart needed"; + return "current"; +} + +export function formatVersion(version: string | undefined): string { + return version === undefined || version === "" ? "unknown" : version; +} + +export function installationLabel(installation: PiWebInstallationInfo | undefined): string { + if (installation === undefined) return "installation unknown"; + if (installation.kind === "pi-package") { + const scope = installation.scope === undefined ? "" : ` · ${installation.scope}`; + const source = installation.source ?? "Pi package"; + return `${source}${scope}`; + } + if (installation.kind === "npm-global") return "global npm package"; + if (installation.kind === "local") return "local checkout"; + if (installation.kind === "docker") return installation.dockerMode === "dev" ? "Docker development runtime" : "Docker runtime"; + return "installation unknown"; +} + +export function machineKindLabel(kind: MachineKind): string { + return kind === "local" ? "local machine" : "remote machine"; +} + +export function releaseSummary(release: PiWebReleaseStatus): string { + if (release.updateAvailable) { + return release.latestVersion === undefined || release.latestVersion === "" + ? "Update available" + : `Update available: ${release.latestVersion}`; + } + if (release.error !== undefined && release.error !== "") return `Update check failed: ${release.error}`; + if (release.skipped === true) return "Update check skipped"; + return "Up to date"; +} + +/** One-line component summary used by the panel rows and the clipboard diagnostics. */ +export function componentDetails(component: PiWebComponentStatus): string { + const parts = [ + `running ${formatVersion(component.runtimeVersion)}`, + `installed ${formatVersion(component.installedVersion)}`, + componentHealth(component), + installationLabel(component.installation), + ]; + if (component.installation?.path !== undefined && component.installation.path !== "") parts.push(component.installation.path); + if (component.error !== undefined && component.error !== "") parts.push(`error: ${component.error}`); + return parts.join(" · "); +} + +export function workspaceFlags(workspace: Workspace): string[] { + return [ + workspace.branch === undefined || workspace.branch === "" ? undefined : `branch ${workspace.branch}`, + workspace.isGitWorktree ? "git worktree" : workspace.isGitRepo ? "git repo" : "not a git repo", + workspace.isMain ? "main workspace" : undefined, + ].filter((flag): flag is string => flag !== undefined); +} + +export interface DiagnosticsInput { + status: PiWebStatusResponse | undefined; + machine?: PluginMachine | undefined; + workspace?: Workspace | undefined; +} + +/** Plain-text status block suitable for pasting into a bug report. */ +export function diagnosticsSummary({ status, machine, workspace }: DiagnosticsInput): string { + const lines: string[] = ["PI WEB diagnostics"]; + if (status === undefined) { + lines.push("Status: unavailable"); + } else { + lines.push(`Package: ${status.packageName}`); + lines.push(`${status.components.web.label}: ${componentDetails(status.components.web)}`); + lines.push(`${status.components.sessiond.label}: ${componentDetails(status.components.sessiond)}`); + const checked = status.release.checkedAt === undefined || status.release.skipped === true ? "" : ` (checked ${status.release.checkedAt})`; + lines.push(`Release: ${releaseSummary(status.release)}${checked}`); + lines.push(`Status generated: ${status.generatedAt}`); + } + if (machine !== undefined) lines.push(`Machine: ${machine.name} (${machineKindLabel(machine.kind)})`); + if (workspace === undefined) { + lines.push("Workspace: none selected"); + } else { + lines.push(`Workspace: ${workspace.label} — ${workspace.path} (${workspaceFlags(workspace).join(", ")})`); + } + return lines.join("\n"); +} + +/** Action body: copy the diagnostics summary for the current runtime context. */ +export async function copyDiagnostics(context: PluginRuntimeContext): Promise { + const summary = diagnosticsSummary({ + status: context.state.piWebStatus, + machine: context.state.selectedMachine, + workspace: context.state.selectedWorkspace, + }); + await navigator.clipboard.writeText(summary); +} + +function renderComponent(html: HtmlTemplateTag, component: PiWebComponentStatus): TemplateResult { + const health = componentHealth(component); + return html` +

+ ${component.label} + ${health} + ${componentDetails(component)} +
+ `; +} + +function renderStatusSection(html: HtmlTemplateTag, status: PiWebStatusResponse | undefined): TemplateResult { + if (status === undefined) { + return html` +
+ PI WEB +

PI WEB status is not available yet. It refreshes automatically in the background.

+
+ `; + } + const web = status.components.web; + const messageCount = status.messages.length; + return html` +
+ PI WEB +
+ Version + ${formatVersion(web.runtimeVersion)} + ${web.installedVersion === undefined || web.installedVersion === web.runtimeVersion ? null : html`installed ${formatVersion(web.installedVersion)}`} +
+
+ Package + ${status.packageName} +
+
+ Installation + ${installationLabel(web.installation)} + ${web.installation?.path === undefined || web.installation.path === "" ? null : html`${web.installation.path}`} +
+
+ Release + ${releaseSummary(status.release)} + ${status.release.checkedAt === undefined || status.release.skipped === true ? null : html`checked ${status.release.checkedAt}`} +
+ ${messageCount === 0 ? null : html`

${String(messageCount)} status ${messageCount === 1 ? "message" : "messages"} — open the Updates tab for details.

`} +

Status generated ${status.generatedAt}

+
+
+ Services + ${renderComponent(html, status.components.web)} + ${renderComponent(html, status.components.sessiond)} +
+ `; +} + +function renderMachineSection(html: HtmlTemplateTag, machine: PluginMachine): TemplateResult { + return html` +
+ Machine +
+ Name + ${machine.name} +
+
+ Type + ${machineKindLabel(machine.kind)} +
+
+ `; +} + +function renderWorkspaceSection(html: HtmlTemplateTag, workspace: Workspace): TemplateResult { + return html` +
+ Workspace +
+ Name + ${workspace.label} +
+
+ Path + ${workspace.path} + ${workspaceFlags(workspace).length === 0 ? null : html`${workspaceFlags(workspace).join(" · ")}`} +
+
+ `; +} + +/** Panel body: render the Info tab for the current workspace panel context. */ +export function renderInfoPanel(html: HtmlTemplateTag, context: WorkspacePanelContext): TemplateResult { + return html` + +
Info
+
+ ${renderStatusSection(html, context.state?.piWebStatus)} + ${renderMachineSection(html, context.machine)} + ${renderWorkspaceSection(html, context.workspace)} +
+ `; +} diff --git a/pi-web-plugins/info/pi-web-plugin.test.ts b/pi-web-plugins/info/pi-web-plugin.test.ts new file mode 100644 index 0000000..ec43936 --- /dev/null +++ b/pi-web-plugins/info/pi-web-plugin.test.ts @@ -0,0 +1,71 @@ +import { html, svg } from "lit"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import type { PluginRuntimeContext } from "@jmfederico/pi-web/plugin-api"; +import plugin from "./pi-web-plugin.js"; + +describe("Info plugin copy-diagnostics action", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("copies a diagnostics summary to the clipboard", async () => { + const writeText = vi.fn((text: string) => { void text; return Promise.resolve(); }); + vi.stubGlobal("navigator", { clipboard: { writeText } }); + const action = findCopyDiagnosticsAction(); + const context = runtimeContext({ + state: { + selectedMachine: { id: "local", name: "devbox", kind: "local" }, + selectedWorkspace: { + id: "ws-1", + projectId: "proj-1", + path: "/srv/dev/pi-web", + label: "pi-web", + branch: "main", + isMain: true, + isGitRepo: true, + isGitWorktree: false, + }, + }, + }); + + await action.run(context); + + expect(writeText).toHaveBeenCalledOnce(); + const summary = writeText.mock.calls[0]?.[0]; + expect(summary).toContain("PI WEB diagnostics"); + expect(summary).toContain("Status: unavailable"); + expect(summary).toContain("Machine: devbox (local machine)"); + expect(summary).toContain("Workspace: pi-web — /srv/dev/pi-web (branch main, git repo, main workspace)"); + }); +}); + +function findCopyDiagnosticsAction() { + const action = plugin.activate({ apiVersion: 1, pluginId: "info", html, svg }).contributions.actions?.find((candidate) => candidate.id === "copy-diagnostics"); + if (action === undefined) throw new Error("Expected copy-diagnostics action"); + return action; +} + +function runtimeContext(patch: Partial = {}): PluginRuntimeContext { + const noop = () => undefined; + return { + state: {}, + prompt: { insertText: noop, getText: () => "", getSelection: () => null }, + openActionPalette: noop, + focusPrompt: noop, + addProject: noop, + configureAuth: noop, + logoutAuth: noop, + openThemePicker: noop, + selectMainView: noop, + selectWorkspaceTool: noop, + openTerminal: noop, + refreshFiles: noop, + refreshGit: noop, + refreshAppData: noop, + reloadPage: noop, + startSession: noop, + archiveSession: noop, + stopActiveWork: noop, + ...patch, + }; +} diff --git a/pi-web-plugins/info/pi-web-plugin.ts b/pi-web-plugins/info/pi-web-plugin.ts index 9a5cdfd..921b67a 100644 --- a/pi-web-plugins/info/pi-web-plugin.ts +++ b/pi-web-plugins/info/pi-web-plugin.ts @@ -1,4 +1,12 @@ +// Skeleton of a PI WEB plugin: metadata plus contribution definitions. +// +// Everything the bundled Info panel and action actually render lives in +// infoInternals.ts. That file is replaceable implementation detail — when +// copying this plugin as a starting point, keep this file's shape and swap +// the internals for your own. + import type { PiWebPlugin } from "@jmfederico/pi-web/plugin-api"; +import { copyDiagnostics, renderInfoPanel } from "./infoInternals.js"; const plugin: PiWebPlugin = { apiVersion: 1, @@ -7,14 +15,11 @@ const plugin: PiWebPlugin = { contributions: { actions: [ { - id: "workspace.show-path", - title: "Show Current Workspace Path", + id: "copy-diagnostics", + title: "Copy PI WEB Diagnostics", + description: "Copy version, installation, and status details for this machine, ready to paste into a bug report", group: "Info", - enabled: (context) => context.state.selectedWorkspace !== undefined, - run: (context) => { - const path = context.state.selectedWorkspace?.path ?? "No workspace selected"; - window.alert(path); - }, + run: (context) => copyDiagnostics(context), }, ], workspaceLabels: [ @@ -36,14 +41,7 @@ const plugin: PiWebPlugin = { `, order: 1000, - render: (context) => html` -
Info
-
-

Workspace

-

${context.workspace.label}

-

${context.workspace.path}

-
- `, + render: (context) => renderInfoPanel(html, context), }, ], }, diff --git a/src/plugin-api.ts b/src/plugin-api.ts index 6b43850..b2b31d5 100644 --- a/src/plugin-api.ts +++ b/src/plugin-api.ts @@ -66,6 +66,8 @@ export interface PluginMachine { } export interface PluginRuntimeState { + /** Identity of the currently selected machine. Undefined only on older hosts or before machines load. */ + selectedMachine?: PluginMachine; selectedWorkspace?: Workspace; selectedSession?: unknown; workspaceTool?: string;