diff --git a/.changeset/responsive-package-settings.md b/.changeset/responsive-package-settings.md
index 83d445e..19ae764 100644
--- a/.changeset/responsive-package-settings.md
+++ b/.changeset/responsive-package-settings.md
@@ -2,4 +2,4 @@
"@jmfederico/pi-web": patch
---
-Keep gateway Settings panels responsive while selected-machine Pi packages load or fail separately, and report Pi package-management support through runtime capabilities.
+Keep gateway Settings panels responsive while selected-machine Pi packages load or fail separately, report Pi package-management support through runtime capabilities, and use that capability to guide remote Pi package-management UI when support is known unavailable.
diff --git a/src/client/src/components/PiWebApp.ts b/src/client/src/components/PiWebApp.ts
index e6b7129..fd885eb 100644
--- a/src/client/src/components/PiWebApp.ts
+++ b/src/client/src/components/PiWebApp.ts
@@ -1923,7 +1923,7 @@ export class PiWebApp extends LitElement {
${state.machineDialogOpen ? html` this.submitMachineDialog(input)} .onCancel=${() => { this.setState({ machineDialogOpen: false }); }}>` : null}
${this.sessionCleanupDialog !== undefined ? html` { void this.previewSessionCleanup(request); }} .onRun=${(request: SessionCleanupRequest) => { void this.runSessionCleanup(request); }} .onClose=${() => { this.closeSessionCleanupDialog(); }}>` : null}
${state.themeDialog !== undefined ? html` { this.pickTheme(value); }} .onCancel=${() => { this.setState({ themeDialog: undefined }); }}>` : null}
- ${this.settingsSection !== undefined ? html` { this.navigateSettings(section); }} .onClose=${() => { this.closeSettings(); }} .onConfigSaved=${(config: PiWebConfigValues) => { this.applyClientConfig(config); }}>` : null}
+ ${this.settingsSection !== undefined ? html` { this.navigateSettings(section); }} .onClose=${() => { this.closeSettings(); }} .onConfigSaved=${(config: PiWebConfigValues) => { this.applyClientConfig(config); }}>` : null}
`;
}
diff --git a/src/client/src/components/SettingsDialog.ts b/src/client/src/components/SettingsDialog.ts
index 85278ce..6fde1ae 100644
--- a/src/client/src/components/SettingsDialog.ts
+++ b/src/client/src/components/SettingsDialog.ts
@@ -1,14 +1,14 @@
import { css, html, LitElement, type PropertyValues, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import type { AppAction } from "../actions";
-import { configApi, piPackagesApi, pluginsApi, type Machine, type PiPackageMutationResponse, type PiPackageScope, type PiPackagesResponse, type PiWebConfigResponse, type PiWebConfigValues, type PiWebPluginsResponse } from "../api";
+import { configApi, piPackagesApi, pluginsApi, type Machine, type MachineRuntime, type PiPackageMutationResponse, type PiPackageScope, type PiPackagesResponse, type PiWebConfigResponse, type PiWebConfigValues, type PiWebPluginsResponse } from "../api";
import type { SettingsSection } from "../settingsRoute";
import "./settings/SettingsGeneralPanel";
import "./settings/SettingsSessiondPanel";
import "./settings/SettingsPackagesPanel";
import "./settings/SettingsPluginsPanel";
import "./settings/SettingsShortcutsPanel";
-import { friendlyPiPackageErrorMessage, piPackageMutationFollowUpMessage, piPackageTargetContext, piPackageTargetLabel, shouldRefreshGatewayPluginsAfterPiPackageMutation, type PiPackageOperationState, type PiPackageTargetContext } from "./settings/piPackageSettings";
+import { friendlyPiPackageErrorMessage, isPiPackageManagementUnsupported, piPackageManagementSupport, piPackageManagementSupportKey, piPackageMutationFollowUpMessage, piPackageTargetContext, piPackageTargetLabel, shouldRefreshGatewayPluginsAfterPiPackageMutation, type PiPackageManagementSupport, type PiPackageOperationState, type PiPackageTargetContext } from "./settings/piPackageSettings";
import { loadGatewaySettingsData, loadPiPackagesData } from "./settings/settingsDataLoading";
@customElement("settings-dialog")
@@ -16,6 +16,7 @@ export class SettingsDialog extends LitElement {
@property({ attribute: false }) section: SettingsSection = "general";
@property({ attribute: false }) actions: AppAction[] = [];
@property({ attribute: false }) machine: Machine | undefined;
+ @property({ attribute: false }) machineRuntime: MachineRuntime | undefined;
@property({ attribute: false }) onNavigate?: (section: SettingsSection) => void;
@property({ attribute: false }) onClose?: () => void;
@property({ attribute: false }) onConfigSaved?: (config: PiWebConfigValues) => void;
@@ -48,10 +49,18 @@ export class SettingsDialog extends LitElement {
}
protected override updated(changed: PropertyValues): void {
- if (!changed.has("machine")) return;
- const previousTarget = piPackageTargetContext(changed.get("machine"));
const currentTarget = this.packageTarget();
- if (previousTarget.id === currentTarget.id) return;
+ if (changed.has("machine")) {
+ const previousTarget = piPackageTargetContext(changed.get("machine"));
+ if (previousTarget.id !== currentTarget.id) {
+ this.resetPackageStateForTargetChange();
+ if (this.isConnected) void this.loadPackagesForTarget(currentTarget);
+ return;
+ }
+ }
+
+ if (!changed.has("machineRuntime")) return;
+ if (!this.packageManagementSupportNeedsReload(changed.get("machineRuntime"), currentTarget)) return;
this.resetPackageStateForTargetChange();
if (this.isConnected) void this.loadPackagesForTarget(currentTarget);
}
@@ -118,6 +127,7 @@ export class SettingsDialog extends LitElement {
piPackagesApi.packages(targetId));
+ const result = await loadPiPackagesData(target, (targetId) => piPackagesApi.packages(targetId), this.packageManagementSupport(target));
if (!this.isCurrentPackageLoad(requestSeq, target)) return;
this.packagesResponse = result.packagesResponse;
@@ -269,6 +279,11 @@ export class SettingsDialog extends LitElement {
}
private async runPiPackageMutation(operation: PiPackageOperationState, label: string, target: PiPackageTargetContext, mutate: () => Promise): Promise {
+ const support = this.packageManagementSupport(target);
+ if (isPiPackageManagementUnsupported(support)) {
+ this.packageError = support.message ?? `Pi package management is not available on ${piPackageTargetLabel(target)}.`;
+ throw new Error(this.packageError);
+ }
if (this.saving) throw new Error("A settings operation is already running.");
const requestSeq = ++this.packageMutationSeq;
this.packageLoadRequestSeq += 1;
@@ -309,6 +324,17 @@ export class SettingsDialog extends LitElement {
return piPackageTargetContext(this.machine);
}
+ private packageManagementSupport(target = this.packageTarget()): PiPackageManagementSupport {
+ return piPackageManagementSupport(target, this.machineRuntime);
+ }
+
+ private packageManagementSupportNeedsReload(previousRuntime: MachineRuntime | undefined, target: PiPackageTargetContext): boolean {
+ const previousSupport = piPackageManagementSupport(target, previousRuntime);
+ const currentSupport = this.packageManagementSupport(target);
+ if (piPackageManagementSupportKey(previousSupport) === piPackageManagementSupportKey(currentSupport)) return false;
+ return previousSupport.state === "unsupported" || currentSupport.state === "unsupported";
+ }
+
private isCurrentLoad(requestSeq: number): boolean {
return requestSeq === this.loadRequestSeq;
}
diff --git a/src/client/src/components/settings/SettingsPackagesPanel.ts b/src/client/src/components/settings/SettingsPackagesPanel.ts
index d133efe..865b022 100644
--- a/src/client/src/components/settings/SettingsPackagesPanel.ts
+++ b/src/client/src/components/settings/SettingsPackagesPanel.ts
@@ -1,7 +1,7 @@
import { css, html, LitElement, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import type { PiPackageInfo, PiPackageScope, PiPackagesResponse } from "../../api";
-import { isPiPackageOperationPending, normalizePiPackageSource, piPackageFilteredLabel, piPackageInstalledPathLabel, piPackageScopeLabel, piPackageSourceValidationMessage, piPackageTargetContext, piPackageTargetLabel, piPackageUpdateDisabledReason, updateAllPiPackagesDisabledReason, type PiPackageOperationState, type PiPackageTargetContext } from "./piPackageSettings";
+import { isPiPackageManagementUnsupported, isPiPackageOperationPending, normalizePiPackageSource, piPackageFilteredLabel, piPackageInstalledPathLabel, piPackageScopeLabel, piPackageSourceValidationMessage, piPackageTargetContext, piPackageTargetLabel, piPackageUpdateDisabledReason, updateAllPiPackagesDisabledReason, type PiPackageManagementSupport, type PiPackageOperationState, type PiPackageTargetContext } from "./piPackageSettings";
@customElement("settings-packages-panel")
export class SettingsPackagesPanel extends LitElement {
@@ -9,6 +9,7 @@ export class SettingsPackagesPanel extends LitElement {
@property({ type: Boolean }) loading = false;
@property({ attribute: false }) operation: PiPackageOperationState | undefined;
@property({ attribute: false }) targetMachine: PiPackageTargetContext | undefined;
+ @property({ attribute: false }) managementSupport: PiPackageManagementSupport | undefined;
@property() error = "";
@property() operationMessage = "";
@property({ attribute: false }) onReload?: () => void | Promise;
@@ -22,21 +23,23 @@ export class SettingsPackagesPanel extends LitElement {
const packages = this.packagesResponse?.packages ?? [];
const target = this.packageTarget;
const targetLabel = piPackageTargetLabel(target);
+ const packageManagementUnavailable = this.packageManagementUnavailable;
return html`
Pi packages
Managing Pi packages on ${targetLabel}. Install, remove, and update packages managed by Pi on the selected machine. Pi packages can provide extensions, skills, prompt templates, themes, context/system prompt files, and PI WEB browser plugins.
-
+
Trusted code warning: Pi packages and PI WEB plugins can run with your user permissions. Install packages and enable plugins only from sources you trust.