Archived
feat: add OMP runtime support
This commit is contained in:
@@ -7,15 +7,15 @@ describe("API parsers", () => {
|
||||
expect(parsePiWebConfigResponse({
|
||||
path: "/tmp/config.json",
|
||||
exists: true,
|
||||
config: { host: "0.0.0.0", port: 8504, allowedHosts: ["example.local"], shortcuts: { "core:view.chat": "mod+1", "core:session.stop": null }, plugins: { info: { enabled: false, settings: { compact: true } } }, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: "manual/uploads" }, maxUploadBytes: 1234 },
|
||||
effectiveConfig: { host: "127.0.0.1", port: 8504, allowedHosts: true, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: ".pi-web/uploads" } },
|
||||
envOverrides: { host: true, port: false, allowedHosts: false, spawnSessions: false, subsessions: false },
|
||||
config: { host: "0.0.0.0", port: 8504, allowedHosts: ["example.local"], shortcuts: { "core:view.chat": "mod+1", "core:session.stop": null }, plugins: { info: { enabled: false, settings: { compact: true } } }, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: "manual/uploads" }, maxUploadBytes: 1234, agent: { command: "omp", dir: "~/.omp/agent" } },
|
||||
effectiveConfig: { host: "127.0.0.1", port: 8504, allowedHosts: true, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: ".pi-web/uploads" }, agent: { command: "omp", dir: "/Users/dev/.omp/agent" } },
|
||||
envOverrides: { host: true, port: false, allowedHosts: false, spawnSessions: false, subsessions: false, agentCommand: false, agentDir: true, agentSessionDir: false },
|
||||
})).toEqual({
|
||||
path: "/tmp/config.json",
|
||||
exists: true,
|
||||
config: { host: "0.0.0.0", port: 8504, allowedHosts: ["example.local"], shortcuts: { "core:view.chat": "mod+1", "core:session.stop": null }, plugins: { info: { enabled: false, settings: { compact: true } } }, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: "manual/uploads" }, maxUploadBytes: 1234 },
|
||||
effectiveConfig: { host: "127.0.0.1", port: 8504, allowedHosts: true, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: ".pi-web/uploads" } },
|
||||
envOverrides: { host: true, port: false, allowedHosts: false, spawnSessions: false, subsessions: false },
|
||||
config: { host: "0.0.0.0", port: 8504, allowedHosts: ["example.local"], shortcuts: { "core:view.chat": "mod+1", "core:session.stop": null }, plugins: { info: { enabled: false, settings: { compact: true } } }, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: "manual/uploads" }, maxUploadBytes: 1234, agent: { command: "omp", dir: "~/.omp/agent" } },
|
||||
effectiveConfig: { host: "127.0.0.1", port: 8504, allowedHosts: true, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: ".pi-web/uploads" }, agent: { command: "omp", dir: "/Users/dev/.omp/agent" } },
|
||||
envOverrides: { host: true, port: false, allowedHosts: false, spawnSessions: false, subsessions: false, agentCommand: false, agentDir: true, agentSessionDir: false },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -531,11 +531,21 @@ function parsePiWebConfigValues(value: unknown): PiWebConfigValues {
|
||||
...optionalField("pathAccess", optionalPathAccess(record["pathAccess"])),
|
||||
...optionalField("uploads", optionalUploads(record["uploads"])),
|
||||
...optionalField("maxUploadBytes", optionalNumber(record, "maxUploadBytes")),
|
||||
...optionalField("agent", optionalAgent(record["agent"])),
|
||||
...optionalField("spawnSessions", optionalBoolean(record, "spawnSessions")),
|
||||
...optionalField("subsessions", optionalBoolean(record, "subsessions")),
|
||||
};
|
||||
}
|
||||
|
||||
function optionalAgent(value: unknown): PiWebConfigValues["agent"] | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (!isRecord(value) || Array.isArray(value)) throw new Error("Invalid PI WEB agent field");
|
||||
return {
|
||||
...optionalField("command", optionalString(value, "command")),
|
||||
...optionalField("dir", optionalString(value, "dir")),
|
||||
};
|
||||
}
|
||||
|
||||
function optionalAllowedHosts(value: unknown): PiWebConfigValues["allowedHosts"] | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
if (value === true) return true;
|
||||
@@ -598,7 +608,16 @@ function optionalPlugins(value: unknown): PiWebPluginConfigMap | undefined {
|
||||
|
||||
function parsePiWebConfigEnvOverrides(value: unknown): PiWebConfigEnvOverrides {
|
||||
const record = requireRecord(value);
|
||||
return { host: requireBoolean(record, "host"), port: requireBoolean(record, "port"), allowedHosts: requireBoolean(record, "allowedHosts"), spawnSessions: requireBoolean(record, "spawnSessions"), subsessions: requireBoolean(record, "subsessions") };
|
||||
return {
|
||||
host: requireBoolean(record, "host"),
|
||||
port: requireBoolean(record, "port"),
|
||||
allowedHosts: requireBoolean(record, "allowedHosts"),
|
||||
spawnSessions: requireBoolean(record, "spawnSessions"),
|
||||
subsessions: requireBoolean(record, "subsessions"),
|
||||
agentCommand: optionalBoolean(record, "agentCommand") ?? false,
|
||||
agentDir: optionalBoolean(record, "agentDir") ?? false,
|
||||
agentSessionDir: optionalBoolean(record, "agentSessionDir") ?? false,
|
||||
};
|
||||
}
|
||||
|
||||
export function parsePiWebPluginsResponse(value: unknown): PiWebPluginsResponse {
|
||||
|
||||
@@ -54,13 +54,13 @@ export class AuthDialog extends LitElement {
|
||||
case "method": return html`
|
||||
<div class="options">
|
||||
<button @click=${() => { this.onChooseMethod?.("oauth"); }}><span>Use a subscription</span><small>ChatGPT Plus/Pro, Claude Pro/Max, or GitHub Copilot</small></button>
|
||||
<button @click=${() => { this.onChooseMethod?.("api_key"); }}><span>Use an API key</span><small>Store an API key in pi auth.json</small></button>
|
||||
<button @click=${() => { this.onChooseMethod?.("api_key"); }}><span>Use an API key</span><small>Store an API key in the configured agent auth.json</small></button>
|
||||
</div>
|
||||
`;
|
||||
case "providers": return html`<div class="options">${state.providers.length === 0 ? html`<div class="empty">No providers available.</div>` : state.providers.map((provider) => this.renderProviderButton(provider))}</div>`;
|
||||
case "apiKey": return html`
|
||||
<div class="form">
|
||||
<p>Enter the API key for <strong>${state.provider.name}</strong>. It will be stored by pi in <code>auth.json</code>.</p>
|
||||
<p>Enter the API key for <strong>${state.provider.name}</strong>. It will be stored in the configured agent <code>auth.json</code>.</p>
|
||||
<input type="password" autocomplete="off" placeholder="API key" .value=${state.value} @input=${(event: Event) => { if (event.target instanceof HTMLInputElement) this.onApiKeyInput?.(event.target.value); }}>
|
||||
${state.error !== undefined && state.error !== "" ? html`<div class="error-text">${state.error}</div>` : null}
|
||||
<div class="actions"><button @click=${() => { this.cancel(); }}>Cancel</button><button class="primary" ?disabled=${state.saving === true} @click=${() => { this.onSaveApiKey?.(); }}>${state.saving === true ? "Saving…" : "Save API key"}</button></div>
|
||||
|
||||
@@ -21,6 +21,9 @@ export class SettingsSessiondPanel extends LitElement {
|
||||
const subsessionsOverridden = config?.envOverrides.subsessions === true;
|
||||
// Beta, off by default; also requires spawn to be enabled.
|
||||
const effectiveSubsessions = config?.effectiveConfig.subsessions === true && effectiveSpawn;
|
||||
const agentCommandOverridden = config?.envOverrides.agentCommand === true;
|
||||
const agentDirOverridden = config?.envOverrides.agentDir === true;
|
||||
const effectiveAgent = config?.effectiveConfig.agent;
|
||||
return html`
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
@@ -36,6 +39,40 @@ export class SettingsSessiondPanel extends LitElement {
|
||||
<span>Config file</span>
|
||||
<code>${config?.path ?? "Unknown"}</code>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-heading">
|
||||
<span>Agent command for diagnostics</span>
|
||||
${agentCommandOverridden ? html`<span class="override-badge">environment override</span>` : null}
|
||||
</span>
|
||||
<input
|
||||
class="text-input"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
.value=${config?.config.agent?.command ?? ""}
|
||||
placeholder="pi"
|
||||
?disabled=${this.loading || this.saving || agentCommandOverridden}
|
||||
@change=${(event: Event) => { void this.saveAgentField("command", event); }}
|
||||
>
|
||||
<small>Use <code>omp</code> to make doctor/update checks target Oh My Pi. The embedded session runtime remains PI WEB's SDK path, so this does not dynamically load a different agent implementation.</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-heading">
|
||||
<span>Agent state directory</span>
|
||||
${agentDirOverridden ? html`<span class="override-badge">environment override</span>` : null}
|
||||
</span>
|
||||
<input
|
||||
class="text-input"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
.value=${config?.config.agent?.dir ?? ""}
|
||||
placeholder="~/.pi/agent or ~/.omp/agent"
|
||||
?disabled=${this.loading || this.saving || agentDirOverridden}
|
||||
@change=${(event: Event) => { void this.saveAgentField("dir", event); }}
|
||||
>
|
||||
<small>Choose which compatible auth, models, settings, and sessions PI WEB reads. For OMP, set this to <code>~/.omp/agent</code>, then restart the session daemon.</small>
|
||||
</div>
|
||||
<div class="field">
|
||||
<span class="field-heading">
|
||||
<span>Allow agents to start sessions</span>
|
||||
@@ -72,6 +109,8 @@ export class SettingsSessiondPanel extends LitElement {
|
||||
<section class="effective-card" aria-label="Effective configuration summary">
|
||||
<h3>Effective after environment overrides</h3>
|
||||
<dl>
|
||||
<div><dt>Agent command</dt><dd>${effectiveAgent?.command ?? html`<span class="muted">pi default</span>`}</dd></div>
|
||||
<div><dt>Agent state</dt><dd>${effectiveAgent?.dir ?? html`<span class="muted">Pi default</span>`}</dd></div>
|
||||
<div><dt>Spawn sessions</dt><dd>${effectiveSpawn ? "Enabled" : html`<span class="muted">Disabled</span>`}</dd></div>
|
||||
<div><dt>Subsessions</dt><dd>${effectiveSubsessions ? "Enabled" : html`<span class="muted">Disabled</span>`}</dd></div>
|
||||
</dl>
|
||||
@@ -86,6 +125,28 @@ export class SettingsSessiondPanel extends LitElement {
|
||||
return null;
|
||||
}
|
||||
|
||||
private async saveAgentField(field: "command" | "dir", event: Event): Promise<void> {
|
||||
if (!(event.target instanceof HTMLInputElement)) return;
|
||||
const value = event.target.value.trim();
|
||||
const baseConfig = this.configResponse?.config ?? {};
|
||||
const nextConfig: PiWebConfigValues = { ...baseConfig };
|
||||
const nextAgent: NonNullable<PiWebConfigValues["agent"]> = { ...(baseConfig.agent ?? {}) };
|
||||
if (field === "command") {
|
||||
if (value === "") delete nextAgent.command;
|
||||
else nextAgent.command = value;
|
||||
} else if (value === "") {
|
||||
delete nextAgent.dir;
|
||||
} else {
|
||||
nextAgent.dir = value;
|
||||
}
|
||||
if (nextAgent.command === undefined && nextAgent.dir === undefined) {
|
||||
delete nextConfig.agent;
|
||||
} else {
|
||||
nextConfig.agent = nextAgent;
|
||||
}
|
||||
await this.onSave?.(nextConfig);
|
||||
}
|
||||
|
||||
private async toggleSpawnSessions(event: Event): Promise<void> {
|
||||
const enabled = event.target instanceof HTMLInputElement && event.target.checked;
|
||||
const baseConfig = this.configResponse?.config ?? {};
|
||||
@@ -124,6 +185,8 @@ export class SettingsSessiondPanel extends LitElement {
|
||||
.field-heading { display: flex; align-items: center; gap: 8px; }
|
||||
.toggle { display: flex; align-items: center; gap: 9px; cursor: pointer; }
|
||||
.toggle input { width: 16px; height: 16px; }
|
||||
.text-input { width: 100%; box-sizing: border-box; border: 1px solid var(--pi-border); border-radius: 8px; background: var(--pi-bg); color: var(--pi-text); padding: 8px 9px; font: 13px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
|
||||
.text-input:disabled { opacity: .65; cursor: not-allowed; }
|
||||
.toggle input:disabled { cursor: not-allowed; }
|
||||
.override-badge { border: 1px solid var(--pi-warning-border); border-radius: 999px; color: var(--pi-warning); background: var(--pi-warning-surface); padding: 2px 7px; font-size: 11px; font-weight: 600; text-transform: none; }
|
||||
.beta-badge { border: 1px solid var(--pi-border); border-radius: 999px; color: var(--pi-muted); background: var(--pi-bg); padding: 2px 7px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: .04em; }
|
||||
|
||||
@@ -20,7 +20,7 @@ describe("settings config drafts", () => {
|
||||
allowedHostsMode: "list",
|
||||
allowedHostsText: "example.local, 192.168.1.20\n",
|
||||
allowedPathsText: "/tmp\n~/SDKs\n",
|
||||
}, { shortcuts: { "core:view.chat": "mod+1", "core:session.stop": null }, plugins: { info: { enabled: false } }, pathAccess: { allowedPaths: ["/old"] }, uploads: { defaultFolder: "manual/uploads" }, maxUploadBytes: 1234 })).toEqual({
|
||||
}, { shortcuts: { "core:view.chat": "mod+1", "core:session.stop": null }, plugins: { info: { enabled: false } }, pathAccess: { allowedPaths: ["/old"] }, uploads: { defaultFolder: "manual/uploads" }, maxUploadBytes: 1234, agent: { command: "omp", dir: "~/.omp/agent" } })).toEqual({
|
||||
host: "127.0.0.1",
|
||||
port: 9000,
|
||||
allowedHosts: ["example.local", "192.168.1.20"],
|
||||
@@ -29,6 +29,7 @@ describe("settings config drafts", () => {
|
||||
pathAccess: { allowedPaths: ["/tmp", "~/SDKs"] },
|
||||
uploads: { defaultFolder: "manual/uploads" },
|
||||
maxUploadBytes: 1234,
|
||||
agent: { command: "omp", dir: "~/.omp/agent" },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export function configFromDraft(draft: ConfigDraft, baseConfig: PiWebConfigValue
|
||||
...(baseConfig.maxUploadBytes === undefined ? {} : { maxUploadBytes: baseConfig.maxUploadBytes }),
|
||||
...(baseConfig.spawnSessions === undefined ? {} : { spawnSessions: baseConfig.spawnSessions }),
|
||||
...(baseConfig.subsessions === undefined ? {} : { subsessions: baseConfig.subsessions }),
|
||||
...(baseConfig.agent === undefined ? {} : { agent: baseConfig.agent }),
|
||||
};
|
||||
const host = draft.host.trim();
|
||||
const port = draft.port.trim();
|
||||
|
||||
Reference in New Issue
Block a user