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).
This commit is contained in:
Federico Jaramillo Martinez
2026-07-27 18:03:25 +02:00
parent 0a35748478
commit 8517800a24
9 changed files with 496 additions and 25 deletions
+5
View File
@@ -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.
@@ -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.
+5 -1
View File
@@ -231,13 +231,17 @@ After editing, check the manifest endpoint and browser-console failure cases.</c
</p>
<ul>
<li><code>pi-web-plugins/info/package.json</code> shows the required metadata shape.</li>
<li><code>pi-web-plugins/info/pi-web-plugin.ts</code> shows the TypeScript source shape.</li>
<li><code>pi-web-plugins/info/pi-web-plugin.ts</code> is the skeleton: metadata and contribution definitions.</li>
<li><code>pi-web-plugins/info/infoInternals.ts</code> holds the bundled panel and action implementation, which you can ignore or replace when copying the plugin.</li>
<li><code>dist/pi-web-plugins/info/pi-web-plugin.js</code> is the built browser module in a checkout.</li>
</ul>
<p>
Read it on GitHub:
<a href="https://github.com/jmfederico/pi-web/tree/main/pi-web-plugins/info">pi-web-plugins/info</a>.
If you copy it, choose a new plugin id so it does not conflict with the bundled <code>info</code> plugin.
Its panel also doubles as an always-available PI WEB status view: it renders the host-provided
<code>context.state.piWebStatus</code> without issuing its own requests, and its action copies a
plain-text diagnostics summary suitable for bug reports.
</p>
<p>
The bundled <code>updates</code> plugin demonstrates dynamic <code>visible</code> and <code>badge</code>
+14 -9
View File
@@ -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.
+162
View File
@@ -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> = {}): 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> = {}): 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> = {}): 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,
};
}
+219
View File
@@ -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<void> {
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`
<div class="info-component">
<strong>${component.label}</strong>
<span class=${health === "current" ? "info-health-ok" : "info-health-attention"}>${health}</span>
<small>${componentDetails(component)}</small>
</div>
`;
}
function renderStatusSection(html: HtmlTemplateTag, status: PiWebStatusResponse | undefined): TemplateResult {
if (status === undefined) {
return html`
<section>
<strong>PI WEB</strong>
<p class="muted">PI WEB status is not available yet. It refreshes automatically in the background.</p>
</section>
`;
}
const web = status.components.web;
const messageCount = status.messages.length;
return html`
<section>
<strong>PI WEB</strong>
<div class="info-row">
<span>Version</span>
<span>${formatVersion(web.runtimeVersion)}</span>
${web.installedVersion === undefined || web.installedVersion === web.runtimeVersion ? null : html`<small>installed ${formatVersion(web.installedVersion)}</small>`}
</div>
<div class="info-row">
<span>Package</span>
<span>${status.packageName}</span>
</div>
<div class="info-row">
<span>Installation</span>
<span>${installationLabel(web.installation)}</span>
${web.installation?.path === undefined || web.installation.path === "" ? null : html`<small>${web.installation.path}</small>`}
</div>
<div class="info-row">
<span>Release</span>
<span>${releaseSummary(status.release)}</span>
${status.release.checkedAt === undefined || status.release.skipped === true ? null : html`<small>checked ${status.release.checkedAt}</small>`}
</div>
${messageCount === 0 ? null : html`<p class="muted">${String(messageCount)} status ${messageCount === 1 ? "message" : "messages"} — open the Updates tab for details.</p>`}
<p class="muted">Status generated ${status.generatedAt}</p>
</section>
<section>
<strong>Services</strong>
${renderComponent(html, status.components.web)}
${renderComponent(html, status.components.sessiond)}
</section>
`;
}
function renderMachineSection(html: HtmlTemplateTag, machine: PluginMachine): TemplateResult {
return html`
<section>
<strong>Machine</strong>
<div class="info-row">
<span>Name</span>
<span>${machine.name}</span>
</div>
<div class="info-row">
<span>Type</span>
<span>${machineKindLabel(machine.kind)}</span>
</div>
</section>
`;
}
function renderWorkspaceSection(html: HtmlTemplateTag, workspace: Workspace): TemplateResult {
return html`
<section>
<strong>Workspace</strong>
<div class="info-row">
<span>Name</span>
<span>${workspace.label}</span>
</div>
<div class="info-row">
<span>Path</span>
<span class="info-path">${workspace.path}</span>
${workspaceFlags(workspace).length === 0 ? null : html`<small>${workspaceFlags(workspace).join(" · ")}</small>`}
</div>
</section>
`;
}
/** Panel body: render the Info tab for the current workspace panel context. */
export function renderInfoPanel(html: HtmlTemplateTag, context: WorkspacePanelContext): TemplateResult {
return html`
<style>
.viewer.info-status { flex: 1 1 auto; min-height: 0; box-sizing: border-box; display: flex; flex-direction: column; gap: 14px; padding: 12px; overflow-y: auto; overflow-x: hidden; }
.viewer.info-status section { flex: 0 0 auto; min-width: 0; display: grid; gap: 8px; align-content: start; }
.viewer.info-status p { margin: 0; }
.info-row { display: grid; grid-template-columns: minmax(90px, auto) minmax(0, 1fr); gap: 3px 10px; border-bottom: 1px solid var(--pi-border-muted); padding: 6px 0; overflow-wrap: anywhere; }
.info-row small { grid-column: 1 / -1; color: var(--pi-muted); }
.info-component { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 3px 10px; border-bottom: 1px solid var(--pi-border-muted); padding: 6px 0; }
.info-component small { grid-column: 1 / -1; color: var(--pi-muted); overflow-wrap: anywhere; }
.info-health-ok { color: var(--pi-success); }
.info-health-attention { color: var(--pi-warning); }
</style>
<section class="toolbar"><strong>Info</strong></section>
<section class="viewer info-status">
${renderStatusSection(html, context.state?.piWebStatus)}
${renderMachineSection(html, context.machine)}
${renderWorkspaceSection(html, context.workspace)}
</section>
`;
}
+71
View File
@@ -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> = {}): 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,
};
}
+13 -15
View File
@@ -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 = {
</svg>
`,
order: 1000,
render: (context) => html`
<section class="toolbar"><strong>Info</strong></section>
<section class="viewer">
<p><strong>Workspace</strong></p>
<p class="muted">${context.workspace.label}</p>
<p class="muted">${context.workspace.path}</p>
</section>
`,
render: (context) => renderInfoPanel(html, context),
},
],
},
+2
View File
@@ -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;