feat: generalize agent runtime config

This commit is contained in:
Federico Jaramillo Martinez
2026-06-28 16:36:40 +02:00
parent 84a485d62e
commit 75e2377756
22 changed files with 289 additions and 155 deletions
@@ -0,0 +1,5 @@
---
"@jmfederico/pi-web": patch
---
Add configurable agent runtime settings so PI WEB can use alternate Pi-compatible commands and isolated agent state/session directories, with web-side status and plugin views re-reading saved config.
-5
View File
@@ -1,5 +0,0 @@
---
"@jmfederico/pi-web": minor
---
Add configurable Pi-compatible agent runtime settings so PI WEB can target Oh My Pi (`omp`) state, auth, sessions, diagnostics, and update checks.
+25 -19
View File
@@ -131,8 +131,7 @@
Environment overrides include <code>PI_WEB_HOST</code>, <code>PI_WEB_PORT</code> / <code>PORT</code>, Environment overrides include <code>PI_WEB_HOST</code>, <code>PI_WEB_PORT</code> / <code>PORT</code>,
<code>PI_WEB_ALLOWED_HOSTS</code>, <code>PI_WEB_MAX_UPLOAD_BYTES</code>, <code>PI_WEB_AGENT_COMMAND</code>, <code>PI_WEB_ALLOWED_HOSTS</code>, <code>PI_WEB_MAX_UPLOAD_BYTES</code>, <code>PI_WEB_AGENT_COMMAND</code>,
<code>PI_WEB_AGENT_DIR</code>, <code>PI_WEB_AGENT_SESSION_DIR</code>, <code>PI_CODING_AGENT_DIR</code>, <code>PI_WEB_AGENT_DIR</code>, <code>PI_WEB_AGENT_SESSION_DIR</code>, <code>PI_CODING_AGENT_DIR</code>,
<code>PI_CODING_AGENT_SESSION_DIR</code>, <code>OMP_CODING_AGENT_DIR</code>, <code>PI_CODING_AGENT_SESSION_DIR</code>, <code>PI_WEB_SPAWN_SESSIONS</code>, and
<code>OMP_CODING_AGENT_SESSION_DIR</code>, <code>PI_WEB_SPAWN_SESSIONS</code>, and
<code>PI_WEB_SUBSESSIONS</code>. <code>PI_WEB_SUBSESSIONS</code>.
</p> </p>
<ul> <ul>
@@ -165,8 +164,8 @@
}, },
"maxUploadBytes": 67108864, "maxUploadBytes": 67108864,
"agent": { "agent": {
"command": "omp", "command": "pi",
"dir": "~/.omp/agent" "dir": "~/agent-profiles/research"
}, },
"spawnSessions": true, "spawnSessions": true,
"subsessions": false, "subsessions": false,
@@ -281,7 +280,7 @@
<tr> <tr>
<td>Agent state directory</td> <td>Agent state directory</td>
<td><code>agent.dir</code></td> <td><code>agent.dir</code></td>
<td><code>PI_WEB_AGENT_DIR</code>, <code>PI_CODING_AGENT_DIR</code>, <code>OMP_CODING_AGENT_DIR</code></td> <td><code>PI_WEB_AGENT_DIR</code>, <code>PI_CODING_AGENT_DIR</code></td>
<td>Global/session daemon</td> <td>Global/session daemon</td>
<td>Not supported locally</td> <td>Not supported locally</td>
<td>Restart session daemon; affects auth, models, settings, and sessions</td> <td>Restart session daemon; affects auth, models, settings, and sessions</td>
@@ -394,7 +393,7 @@
<tr> <tr>
<td>Agent session storage directory</td> <td>Agent session storage directory</td>
<td></td> <td></td>
<td><code>PI_WEB_AGENT_SESSION_DIR</code>, <code>PI_CODING_AGENT_SESSION_DIR</code>, <code>OMP_CODING_AGENT_SESSION_DIR</code></td> <td><code>PI_WEB_AGENT_SESSION_DIR</code>, <code>PI_CODING_AGENT_SESSION_DIR</code></td>
<td>Session daemon env</td> <td>Session daemon env</td>
<td>Not supported locally</td> <td>Not supported locally</td>
<td>Restart session daemon; env-only session storage override</td> <td>Restart session daemon; env-only session storage override</td>
@@ -402,7 +401,7 @@
<tr> <tr>
<td>Agent config directory</td> <td>Agent config directory</td>
<td></td> <td></td>
<td><code>PI_WEB_AGENT_DIR</code>, <code>PI_CODING_AGENT_DIR</code>, <code>OMP_CODING_AGENT_DIR</code></td> <td><code>PI_WEB_AGENT_DIR</code>, <code>PI_CODING_AGENT_DIR</code></td>
<td>Web/API + session daemon env</td> <td>Web/API + session daemon env</td>
<td>Not supported locally</td> <td>Not supported locally</td>
<td>Restart services</td> <td>Restart services</td>
@@ -448,34 +447,41 @@
<h2>Agent runtime</h2> <h2>Agent runtime</h2>
<p> <p>
<code>agent.command</code> controls which Pi-compatible CLI PI WEB checks in doctor/status/update flows. <code>agent.command</code> controls which Pi-compatible CLI PI WEB checks in doctor/status/update flows.
It defaults to <code>pi</code>; set it to <code>omp</code> when this machine should use Oh My Pi. It defaults to <code>pi</code>. Set it only when diagnostics and package-managed update checks should target
another compatible command; the embedded session runtime still uses PI WEB's SDK integration.
</p> </p>
<p> <p>
<code>agent.dir</code> controls which compatible agent state directory PI WEB reads for auth providers, <code>agent.dir</code> controls which compatible agent state directory PI WEB reads for auth providers,
model settings, settings, and session metadata. It defaults to the selected agent's conventional model settings, settings, and session metadata. It defaults to <code>~/.pi/agent</code>. Set it to another
directory (<code>~/.pi/agent</code> for <code>pi</code>, <code>~/.omp/agent</code> for <code>omp</code>). Pi-compatible state directory when you want an isolated profile or an alternate compatible agent's data.
</p> </p>
<div class="code-card"> <div class="code-card">
<pre><code>{ <pre><code>{
"agent": { "agent": {
"command": "omp", "command": "pi",
"dir": "~/.omp/agent" "dir": "~/agent-profiles/research"
} }
}</code></pre> }</code></pre>
</div> </div>
<p> <p>
Environment variables take precedence over the config file. <code>PI_WEB_AGENT_COMMAND</code> selects the For example, an Oh My Pi profile can set <code>agent.command</code> to <code>omp</code> and
command, <code>PI_WEB_AGENT_DIR</code> sets the state directory for any command, and command-specific <code>agent.dir</code> to <code>~/.omp/agent</code>.
variables such as <code>OMP_CODING_AGENT_DIR</code> are honored when the selected command is <code>omp</code>.
</p> </p>
<p> <p>
Session directory overrides are environment-only. Set <code>PI_WEB_AGENT_SESSION_DIR</code> or the selected Environment variables take precedence over the config file. <code>PI_WEB_AGENT_COMMAND</code> selects the
command's session variable (for example <code>OMP_CODING_AGENT_SESSION_DIR</code>) when you need to override command, <code>PI_WEB_AGENT_DIR</code> sets the state directory for any command, and
session storage separately from <code>agent.dir</code>. <code>PI_WEB_AGENT_SESSION_DIR</code> overrides session storage separately from <code>agent.dir</code>.
Existing Pi Coding Agent env names (<code>PI_CODING_AGENT_DIR</code> and
<code>PI_CODING_AGENT_SESSION_DIR</code>) remain supported for compatibility.
</p>
<p>
Session directory overrides are environment-only; use <code>PI_WEB_AGENT_SESSION_DIR</code> unless you need
the legacy Pi-compatible <code>PI_CODING_AGENT_SESSION_DIR</code> name.
</p> </p>
<div class="callout warning"> <div class="callout warning">
Restart the session daemon after changing agent settings. The web/API process can display the new config Restart the session daemon after changing agent settings. The web/API process can display the new config
immediately, but active session runtime ownership is intentionally long-lived. immediately, and status/plugin discovery may re-read it on later requests, but active session runtime
ownership is intentionally long-lived.
</div> </div>
</section> </section>
+15 -13
View File
@@ -25,7 +25,7 @@ defaults → global config file → environment overrides
Supported project-local settings are then applied for that project's workspaces. For upload defaults, `<project>/.pi-web/config.json` overrides the global value. Supported project-local settings are then applied for that project's workspaces. For upload defaults, `<project>/.pi-web/config.json` overrides the global value.
Environment overrides include `PI_WEB_HOST`, `PI_WEB_PORT` / `PORT`, `PI_WEB_ALLOWED_HOSTS`, `PI_WEB_MAX_UPLOAD_BYTES`, `PI_WEB_AGENT_COMMAND`, `PI_WEB_AGENT_DIR`, `PI_WEB_AGENT_SESSION_DIR`, `PI_CODING_AGENT_DIR`, `PI_CODING_AGENT_SESSION_DIR`, `OMP_CODING_AGENT_DIR`, `OMP_CODING_AGENT_SESSION_DIR`, `PI_WEB_SPAWN_SESSIONS`, and `PI_WEB_SUBSESSIONS`. Environment overrides include `PI_WEB_HOST`, `PI_WEB_PORT` / `PORT`, `PI_WEB_ALLOWED_HOSTS`, `PI_WEB_MAX_UPLOAD_BYTES`, `PI_WEB_AGENT_COMMAND`, `PI_WEB_AGENT_DIR`, `PI_WEB_AGENT_SESSION_DIR`, `PI_CODING_AGENT_DIR`, `PI_CODING_AGENT_SESSION_DIR`, `PI_WEB_SPAWN_SESSIONS`, and `PI_WEB_SUBSESSIONS`.
Process restarts depend on the key: Process restarts depend on the key:
@@ -51,8 +51,8 @@ Process restarts depend on the key:
}, },
"maxUploadBytes": 67108864, "maxUploadBytes": 67108864,
"agent": { "agent": {
"command": "omp", "command": "pi",
"dir": "~/.omp/agent" "dir": "~/agent-profiles/research"
}, },
"spawnSessions": true, "spawnSessions": true,
"subsessions": false, "subsessions": false,
@@ -104,7 +104,7 @@ Rows with JSON key `—` are runtime-only environment variables, not config-file
| Manual file upload default folder | `uploads.defaultFolder` | — | Global + project | **Overrides**: project value wins for workspaces in that project; otherwise global/default applies | New Upload dialogs and direct drag/drop batches after config/workspace refresh | | Manual file upload default folder | `uploads.defaultFolder` | — | Global + project | **Overrides**: project value wins for workspaces in that project; otherwise global/default applies | New Upload dialogs and direct drag/drop batches after config/workspace refresh |
| Upload/body limit | `maxUploadBytes` | `PI_WEB_MAX_UPLOAD_BYTES` | Global | Not supported locally | Restart web/API and session daemon | | Upload/body limit | `maxUploadBytes` | `PI_WEB_MAX_UPLOAD_BYTES` | Global | Not supported locally | Restart web/API and session daemon |
| Agent CLI command | `agent.command` | `PI_WEB_AGENT_COMMAND` | Global/session daemon | Not supported locally | Restart session daemon; affects doctor/status/update checks | | Agent CLI command | `agent.command` | `PI_WEB_AGENT_COMMAND` | Global/session daemon | Not supported locally | Restart session daemon; affects doctor/status/update checks |
| Agent state directory | `agent.dir` | `PI_WEB_AGENT_DIR`, `PI_CODING_AGENT_DIR`, `OMP_CODING_AGENT_DIR` | Global/session daemon | Not supported locally | Restart session daemon; affects auth, models, settings, and sessions | | Agent state directory | `agent.dir` | `PI_WEB_AGENT_DIR`, `PI_CODING_AGENT_DIR` | Global/session daemon | Not supported locally | Restart session daemon; affects auth, models, settings, and sessions |
| Agent can spawn sessions | `spawnSessions` | `PI_WEB_SPAWN_SESSIONS` | Global/session daemon | Not supported locally | Restart session daemon | | Agent can spawn sessions | `spawnSessions` | `PI_WEB_SPAWN_SESSIONS` | Global/session daemon | Not supported locally | Restart session daemon |
| Tracked subsessions (beta) | `subsessions` | `PI_WEB_SUBSESSIONS` | Global/session daemon | Not supported locally; also requires `spawnSessions` | Restart session daemon | | Tracked subsessions (beta) | `subsessions` | `PI_WEB_SUBSESSIONS` | Global/session daemon | Not supported locally; also requires `spawnSessions` | Restart session daemon |
| Plugin enablement/settings | `plugins.<id>.enabled`, `plugins.<id>.settings` | — | Global | Not core local config; plugins may read their own project files | Reload browser tab | | Plugin enablement/settings | `plugins.<id>.enabled`, `plugins.<id>.settings` | — | Global | Not core local config; plugins may read their own project files | Reload browser tab |
@@ -119,8 +119,8 @@ Rows with JSON key `—` are runtime-only environment variables, not config-file
| Web-to-daemon URL | — | `PI_WEB_SESSIOND_URL` | Web/API env | Not supported locally | Restart web/API | | Web-to-daemon URL | — | `PI_WEB_SESSIOND_URL` | Web/API env | Not supported locally | Restart web/API |
| Projects storage file | — | `PI_WEB_PROJECTS_FILE` | Web/API + session daemon env | Not supported locally | Restart services; advanced state override | | Projects storage file | — | `PI_WEB_PROJECTS_FILE` | Web/API + session daemon env | Not supported locally | Restart services; advanced state override |
| Remote machines storage file | — | `PI_WEB_MACHINES_FILE` | Web/API env | Not supported locally | Restart web/API; advanced state override | | Remote machines storage file | — | `PI_WEB_MACHINES_FILE` | Web/API env | Not supported locally | Restart web/API; advanced state override |
| Agent session storage directory | — | `PI_WEB_AGENT_SESSION_DIR`, `PI_CODING_AGENT_SESSION_DIR`, `OMP_CODING_AGENT_SESSION_DIR` | Session daemon env | Not supported locally | Restart session daemon; env-only session storage override | | Agent session storage directory | — | `PI_WEB_AGENT_SESSION_DIR`, `PI_CODING_AGENT_SESSION_DIR` | Session daemon env | Not supported locally | Restart session daemon; env-only session storage override |
| Agent config directory | — | `PI_WEB_AGENT_DIR`, `PI_CODING_AGENT_DIR`, `OMP_CODING_AGENT_DIR` | Web/API + session daemon env | Not supported locally | Restart services | | Agent config directory | — | `PI_WEB_AGENT_DIR`, `PI_CODING_AGENT_DIR` | Web/API + session daemon env | Not supported locally | Restart services |
| Skip update checks | — | `PI_WEB_SKIP_VERSION_CHECK`, `PI_WEB_OFFLINE`, `PI_SKIP_VERSION_CHECK`, `PI_OFFLINE` | Web/API env | Not supported locally | Restart web/API after env changes | | Skip update checks | — | `PI_WEB_SKIP_VERSION_CHECK`, `PI_WEB_OFFLINE`, `PI_SKIP_VERSION_CHECK`, `PI_OFFLINE` | Web/API env | Not supported locally | Restart web/API after env changes |
## Key details ## Key details
@@ -168,24 +168,26 @@ The per-request size limit is still controlled by `maxUploadBytes` / `PI_WEB_MAX
### Agent runtime selection ### Agent runtime selection
`agent.command` controls which Pi-compatible CLI PI WEB checks in doctor/status/update flows. It defaults to `pi`; set it to `omp` when this machine should use Oh My Pi. `agent.command` controls which Pi-compatible CLI PI WEB checks in doctor/status/update flows. It defaults to `pi`. Set it only when diagnostics and package-managed update checks should target another compatible command; the embedded session runtime still uses PI WEB's SDK integration.
`agent.dir` controls which compatible agent state directory PI WEB reads for auth providers, model settings, settings, and session metadata. It defaults to the selected agent's conventional directory (`~/.pi/agent` for `pi`, `~/.omp/agent` for `omp`). `agent.dir` controls which compatible agent state directory PI WEB reads for auth providers, model settings, settings, and session metadata. It defaults to `~/.pi/agent`. Set it to another Pi-compatible state directory when you want an isolated profile or an alternate compatible agent's data.
```json ```json
{ {
"agent": { "agent": {
"command": "omp", "command": "pi",
"dir": "~/.omp/agent" "dir": "~/agent-profiles/research"
} }
} }
``` ```
Environment variables take precedence over the config file. `PI_WEB_AGENT_COMMAND` selects the command, `PI_WEB_AGENT_DIR` sets the state directory for any command, and command-specific variables such as `OMP_CODING_AGENT_DIR` are honored when the selected command is `omp`. For example, an Oh My Pi profile can set `agent.command` to `omp` and `agent.dir` to `~/.omp/agent`.
Session directory overrides are environment-only. Set `PI_WEB_AGENT_SESSION_DIR` or the selected command's session variable (for example `OMP_CODING_AGENT_SESSION_DIR`) when you need to override session storage separately from `agent.dir`. Environment variables take precedence over the config file. `PI_WEB_AGENT_COMMAND` selects the command, `PI_WEB_AGENT_DIR` sets the state directory for any command, and `PI_WEB_AGENT_SESSION_DIR` overrides session storage separately from `agent.dir`. Existing Pi Coding Agent env names (`PI_CODING_AGENT_DIR` and `PI_CODING_AGENT_SESSION_DIR`) remain supported for compatibility.
Restart the session daemon after changing agent settings. The web/API process can display the new config immediately, but active session runtime ownership is intentionally long-lived. Session directory overrides are environment-only; use `PI_WEB_AGENT_SESSION_DIR` unless you need the legacy Pi-compatible `PI_CODING_AGENT_SESSION_DIR` name.
Restart the session daemon after changing agent settings. The web/API process can display the new config immediately, and status/plugin discovery may re-read it on later requests, but active session runtime ownership is intentionally long-lived.
### Session daemon tools ### Session daemon tools
+4 -4
View File
@@ -7,14 +7,14 @@ describe("API parsers", () => {
expect(parsePiWebConfigResponse({ expect(parsePiWebConfigResponse({
path: "/tmp/config.json", path: "/tmp/config.json",
exists: true, 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, agent: { command: "omp", dir: "~/.omp/agent" } }, 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: "agent-lab", dir: "~/agent-profiles/lab" } },
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" } }, effectiveConfig: { host: "127.0.0.1", port: 8504, allowedHosts: true, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: ".pi-web/uploads" }, agent: { command: "agent-lab", dir: "/Users/dev/agent-profiles/lab" } },
envOverrides: { host: true, port: false, allowedHosts: false, spawnSessions: false, subsessions: false, agentCommand: false, agentDir: true, agentSessionDir: false }, envOverrides: { host: true, port: false, allowedHosts: false, spawnSessions: false, subsessions: false, agentCommand: false, agentDir: true, agentSessionDir: false },
})).toEqual({ })).toEqual({
path: "/tmp/config.json", path: "/tmp/config.json",
exists: true, 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, agent: { command: "omp", dir: "~/.omp/agent" } }, 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: "agent-lab", dir: "~/agent-profiles/lab" } },
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" } }, effectiveConfig: { host: "127.0.0.1", port: 8504, allowedHosts: true, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: ".pi-web/uploads" }, agent: { command: "agent-lab", dir: "/Users/dev/agent-profiles/lab" } },
envOverrides: { host: true, port: false, allowedHosts: false, spawnSessions: false, subsessions: false, agentCommand: false, agentDir: true, agentSessionDir: false }, envOverrides: { host: true, port: false, allowedHosts: false, spawnSessions: false, subsessions: false, agentCommand: false, agentDir: true, agentSessionDir: false },
}); });
}); });
@@ -54,7 +54,7 @@ export class SettingsSessiondPanel extends LitElement {
?disabled=${this.loading || this.saving || agentCommandOverridden} ?disabled=${this.loading || this.saving || agentCommandOverridden}
@change=${(event: Event) => { void this.saveAgentField("command", event); }} @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> <small>Set an alternate Pi-compatible CLI when doctor/update checks should target a different command. The embedded session runtime remains PI WEB's SDK path, so this does not dynamically load a different agent implementation.</small>
</div> </div>
<div class="field"> <div class="field">
<span class="field-heading"> <span class="field-heading">
@@ -67,11 +67,11 @@ export class SettingsSessiondPanel extends LitElement {
autocomplete="off" autocomplete="off"
spellcheck="false" spellcheck="false"
.value=${config?.config.agent?.dir ?? ""} .value=${config?.config.agent?.dir ?? ""}
placeholder="~/.pi/agent or ~/.omp/agent" placeholder="~/.pi/agent or ~/agent-profiles/work"
?disabled=${this.loading || this.saving || agentDirOverridden} ?disabled=${this.loading || this.saving || agentDirOverridden}
@change=${(event: Event) => { void this.saveAgentField("dir", event); }} @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> <small>Choose which compatible auth, models, settings, and sessions PI WEB reads. Set a separate directory for isolated agent profiles, then restart the session daemon.</small>
</div> </div>
<div class="field"> <div class="field">
<span class="field-heading"> <span class="field-heading">
@@ -20,7 +20,7 @@ describe("settings config drafts", () => {
allowedHostsMode: "list", allowedHostsMode: "list",
allowedHostsText: "example.local, 192.168.1.20\n", allowedHostsText: "example.local, 192.168.1.20\n",
allowedPathsText: "/tmp\n~/SDKs\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, agent: { command: "omp", dir: "~/.omp/agent" } })).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: "agent-lab", dir: "~/agent-profiles/lab" } })).toEqual({
host: "127.0.0.1", host: "127.0.0.1",
port: 9000, port: 9000,
allowedHosts: ["example.local", "192.168.1.20"], allowedHosts: ["example.local", "192.168.1.20"],
@@ -29,7 +29,7 @@ describe("settings config drafts", () => {
pathAccess: { allowedPaths: ["/tmp", "~/SDKs"] }, pathAccess: { allowedPaths: ["/tmp", "~/SDKs"] },
uploads: { defaultFolder: "manual/uploads" }, uploads: { defaultFolder: "manual/uploads" },
maxUploadBytes: 1234, maxUploadBytes: 1234,
agent: { command: "omp", dir: "~/.omp/agent" }, agent: { command: "agent-lab", dir: "~/agent-profiles/lab" },
}); });
}); });
+43 -25
View File
@@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { join } from "node:path"; import { join } from "node:path";
import { tmpdir } from "node:os"; import { tmpdir } from "node:os";
import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { DEFAULT_MAX_UPLOAD_BYTES, DEFAULT_UPLOADS_FOLDER, effectiveAgentConfig, effectivePiWebConfig, loadPiWebConfig, maxUploadBytes, savePiWebConfig, spawnSessionsEnabled, subsessionsEnabled } from "./config.js"; import { DEFAULT_MAX_UPLOAD_BYTES, DEFAULT_UPLOADS_FOLDER, agentSessionDirEnvKeys, effectiveAgentConfig, effectivePiWebConfig, hasAgentDirEnvOverride, hasAgentSessionDirEnvOverride, loadPiWebConfig, maxUploadBytes, savePiWebConfig, spawnSessionsEnabled, subsessionsEnabled } from "./config.js";
let tempDir: string; let tempDir: string;
let configPath: string; let configPath: string;
@@ -55,43 +55,61 @@ describe("PI WEB config persistence", () => {
expect(loadPiWebConfig(testOptions()).config.agent).toEqual({ command: "omp", dir: "~/.omp/agent" }); expect(loadPiWebConfig(testOptions()).config.agent).toEqual({ command: "omp", dir: "~/.omp/agent" });
}); });
it("resolves OMP agent defaults from the configured command", () => { it("keeps the Pi agent directory default for alternate commands", () => {
expect(effectiveAgentConfig({ HOME: join(tempDir, ".home") }, { agent: { command: "omp" } })).toMatchObject({ expect(effectiveAgentConfig({ HOME: join(tempDir, ".home") }, { agent: { command: "omp" } })).toMatchObject({
command: "omp", command: "omp",
dir: join(tempDir, ".home", ".omp", "agent"), dir: join(tempDir, ".home", ".pi", "agent"),
sessionDirEnvKeys: ["PI_WEB_AGENT_SESSION_DIR", "OMP_CODING_AGENT_SESSION_DIR", "PI_CODING_AGENT_SESSION_DIR"], sessionDirEnvKeys: ["PI_WEB_AGENT_SESSION_DIR", "PI_CODING_AGENT_SESSION_DIR"],
}); });
}); });
it("lets PI WEB agent environment overrides take precedence", () => { it("resolves explicit alternate agent command and state directory settings", () => {
expect(effectiveAgentConfig({ HOME: join(tempDir, ".home") }, { agent: { command: "omp", dir: "~/.omp/agent" } })).toMatchObject({
command: "omp",
dir: join(tempDir, ".home", ".omp", "agent"),
});
});
it("ignores empty agent environment overrides", () => {
const env = {
HOME: join(tempDir, ".home"),
PI_WEB_AGENT_COMMAND: "",
PI_WEB_AGENT_DIR: "",
PI_WEB_AGENT_SESSION_DIR: "",
PI_CODING_AGENT_DIR: "",
PI_CODING_AGENT_SESSION_DIR: "",
};
expect(effectiveAgentConfig(env, { agent: { command: "omp", dir: "~/.omp/agent" } })).toMatchObject({
command: "omp",
dir: join(tempDir, ".home", ".omp", "agent"),
});
expect(hasAgentDirEnvOverride(env)).toBe(false);
expect(hasAgentSessionDirEnvOverride(env)).toBe(false);
});
it("uses generic agent directory env precedence and Pi compatibility fallback", () => {
expect(effectiveAgentConfig({ expect(effectiveAgentConfig({
PI_WEB_AGENT_COMMAND: "omp", PI_WEB_AGENT_COMMAND: "omp",
PI_WEB_AGENT_DIR: join(tempDir, "env-agent"), PI_WEB_AGENT_DIR: join(tempDir, "web-env-agent"),
PI_CODING_AGENT_DIR: join(tempDir, "pi-env-agent"),
}, { agent: { command: "pi", dir: join(tempDir, "config-agent") } })).toMatchObject({ }, { agent: { command: "pi", dir: join(tempDir, "config-agent") } })).toMatchObject({
command: "omp", command: "omp",
dir: join(tempDir, "env-agent"), dir: join(tempDir, "web-env-agent"),
});
expect(effectiveAgentConfig({
PI_CODING_AGENT_DIR: join(tempDir, "pi-env-agent"),
}, { agent: { dir: join(tempDir, "config-agent") } })).toMatchObject({
dir: join(tempDir, "pi-env-agent"),
}); });
}); });
it("lets command-specific agent environment directories override config", () => { it("does not generate command-specific session directory env keys", () => {
expect(effectiveAgentConfig({ const keys = ["PI_WEB_AGENT_SESSION_DIR", "PI_CODING_AGENT_SESSION_DIR"];
HOME: join(tempDir, ".home"),
OMP_CODING_AGENT_DIR: join(tempDir, "omp-env-agent"),
}, { agent: { command: "omp", dir: join(tempDir, "config-agent") } })).toMatchObject({
command: "omp",
dir: join(tempDir, "omp-env-agent"),
});
});
it("normalizes omp.exe to OMP environment keys", () => { expect(agentSessionDirEnvKeys()).toEqual(keys);
expect(effectiveAgentConfig({ expect(effectiveAgentConfig({ HOME: join(tempDir, ".home"), PI_WEB_AGENT_COMMAND: "omp" }).sessionDirEnvKeys).toEqual(keys);
HOME: join(tempDir, ".home"),
OMP_CODING_AGENT_DIR: join(tempDir, "omp-exe-env-agent"),
}, { agent: { command: "omp.exe", dir: join(tempDir, "config-agent") } })).toMatchObject({
command: "omp.exe",
dir: join(tempDir, "omp-exe-env-agent"),
sessionDirEnvKeys: ["PI_WEB_AGENT_SESSION_DIR", "OMP_CODING_AGENT_SESSION_DIR", "PI_CODING_AGENT_SESSION_DIR"],
});
}); });
it("exposes the default upload folder in the effective config", () => { it("exposes the default upload folder in the effective config", () => {
+18 -31
View File
@@ -49,26 +49,25 @@ export interface EffectivePiWebAgentConfig {
} }
export function effectiveAgentConfig(env: NodeJS.ProcessEnv = process.env, config: Pick<PiWebConfig, "agent"> = {}, cwd = process.cwd()): EffectivePiWebAgentConfig { export function effectiveAgentConfig(env: NodeJS.ProcessEnv = process.env, config: Pick<PiWebConfig, "agent"> = {}, cwd = process.cwd()): EffectivePiWebAgentConfig {
const command = parseAgentCommand(env[PI_WEB_AGENT_COMMAND_ENV] ?? config.agent?.command ?? DEFAULT_AGENT_COMMAND, "agent.command", "environment"); const command = parseAgentCommand(envValue(env, PI_WEB_AGENT_COMMAND_ENV) ?? config.agent?.command ?? DEFAULT_AGENT_COMMAND, "agent.command", "environment");
const commandDirEnv = commandAgentDirEnv(command); const configuredDir = envValue(env, PI_WEB_AGENT_DIR_ENV) ?? envValue(env, PI_CODING_AGENT_DIR_ENV) ?? config.agent?.dir ?? defaultAgentDir(env);
const configuredDir = env[PI_WEB_AGENT_DIR_ENV] ?? env[commandDirEnv] ?? config.agent?.dir ?? defaultAgentDirForCommand(command, env);
return { return {
command, command,
dir: resolveAgentDirPath(configuredDir, env, cwd, "agent.dir", "environment"), dir: resolveAgentDirPath(configuredDir, env, cwd, "agent.dir", "environment"),
sessionDirEnvKeys: agentSessionDirEnvKeys(command), sessionDirEnvKeys: agentSessionDirEnvKeys(),
}; };
} }
export function agentSessionDirEnvKeys(command = DEFAULT_AGENT_COMMAND): string[] { export function agentSessionDirEnvKeys(): string[] {
return uniqueStrings([PI_WEB_AGENT_SESSION_DIR_ENV, commandSessionDirEnv(command), PI_CODING_AGENT_SESSION_DIR_ENV]); return uniqueStrings([PI_WEB_AGENT_SESSION_DIR_ENV, PI_CODING_AGENT_SESSION_DIR_ENV]);
} }
export function hasAgentDirEnvOverride(env: NodeJS.ProcessEnv, command = DEFAULT_AGENT_COMMAND): boolean { export function hasAgentDirEnvOverride(env: NodeJS.ProcessEnv): boolean {
return isEnvSet(env[PI_WEB_AGENT_DIR_ENV]) || isEnvSet(env[commandAgentDirEnv(command)]); return isEnvSet(env[PI_WEB_AGENT_DIR_ENV]) || isEnvSet(env[PI_CODING_AGENT_DIR_ENV]);
} }
export function hasAgentSessionDirEnvOverride(env: NodeJS.ProcessEnv, command = DEFAULT_AGENT_COMMAND): boolean { export function hasAgentSessionDirEnvOverride(env: NodeJS.ProcessEnv): boolean {
return agentSessionDirEnvKeys(command).some((key) => isEnvSet(env[key])); return agentSessionDirEnvKeys().some((key) => isEnvSet(env[key]));
} }
export function effectiveUploadsConfig(config: Pick<PiWebConfig, "uploads"> = {}): NonNullable<PiWebConfig["uploads"]> { export function effectiveUploadsConfig(config: Pick<PiWebConfig, "uploads"> = {}): NonNullable<PiWebConfig["uploads"]> {
@@ -109,7 +108,10 @@ export function loadPiWebConfig(options: LoadOptions = {}): LoadedPiWebConfig {
} }
export function effectivePiWebConfig(options: LoadOptions = {}): LoadedPiWebConfig { export function effectivePiWebConfig(options: LoadOptions = {}): LoadedPiWebConfig {
const loaded = loadPiWebConfig(options); return resolveEffectivePiWebConfig(loadPiWebConfig(options), options);
}
export function resolveEffectivePiWebConfig(loaded: LoadedPiWebConfig, options: LoadOptions = {}): LoadedPiWebConfig {
const env = options.env ?? process.env; const env = options.env ?? process.env;
const host = env["PI_WEB_HOST"]; const host = env["PI_WEB_HOST"];
const port = env["PI_WEB_PORT"] ?? env["PORT"]; const port = env["PI_WEB_PORT"] ?? env["PORT"];
@@ -333,28 +335,13 @@ function expandHomePath(value: string, env: NodeJS.ProcessEnv): string {
return value; return value;
} }
function defaultAgentDirForCommand(command: string, env: NodeJS.ProcessEnv): string { function defaultAgentDir(env: NodeJS.ProcessEnv): string {
return expandHomePath(isOmpCommand(command) ? "~/.omp/agent" : "~/.pi/agent", env); return expandHomePath("~/.pi/agent", env);
} }
function commandAgentDirEnv(command: string): string { function envValue(env: NodeJS.ProcessEnv, key: string): string | undefined {
const prefix = agentEnvPrefix(command); const value = env[key];
return prefix === "PI" ? PI_CODING_AGENT_DIR_ENV : `${prefix}_CODING_AGENT_DIR`; return value !== undefined && value !== "" ? value : undefined;
}
function commandSessionDirEnv(command: string): string {
return `${agentEnvPrefix(command)}_CODING_AGENT_SESSION_DIR`;
}
function agentEnvPrefix(command: string): string {
const name = command.split(/[\\/]/u).at(-1) ?? command;
const normalized = name.replace(/(?:\.[cm]?js|\.exe)$/iu, "").replace(/[^A-Za-z0-9]+/gu, "_").replace(/^_+|_+$/gu, "").toUpperCase();
return normalized === "" ? "PI" : normalized;
}
function isOmpCommand(command: string): boolean {
const name = command.split(/[\\/]/u).at(-1)?.toLowerCase();
return name === "omp" || name === "omp.exe";
} }
function isEnvSet(value: string | undefined): boolean { function isEnvSet(value: string | undefined): boolean {
+40 -1
View File
@@ -15,7 +15,7 @@ import type { SessionProxyDaemon } from "./sessiond/sessionProxyRoutes.js";
import { PI_WEB_CAPABILITIES } from "../shared/capabilities.js"; import { PI_WEB_CAPABILITIES } from "../shared/capabilities.js";
import { machineScopedPluginId } from "../shared/machinePluginIds.js"; import { machineScopedPluginId } from "../shared/machinePluginIds.js";
import { MAX_IMAGE_PREVIEW_BYTES } from "../shared/workspaceFiles.js"; import { MAX_IMAGE_PREVIEW_BYTES } from "../shared/workspaceFiles.js";
import type { PiWebConfigResponse, PiWebConfigValues } from "../shared/apiTypes.js"; import type { PiWebConfigResponse, PiWebConfigValues, PiWebStatusResponse } from "../shared/apiTypes.js";
import type { Project, Workspace } from "./types.js"; import type { Project, Workspace } from "./types.js";
let app: FastifyInstance; let app: FastifyInstance;
@@ -556,6 +556,35 @@ describe("buildApp", () => {
]); ]);
}); });
it("uses the latest configured agent dir for PI WEB status after config writes", async () => {
const originalSkipVersionCheck = process.env["PI_WEB_SKIP_VERSION_CHECK"];
process.env["PI_WEB_SKIP_VERSION_CHECK"] = "1";
try {
const initialAgentDir = join(tempDir, "initial-agent");
const updatedAgentDir = join(tempDir, "updated-agent");
piWebConfig = { agent: { command: "pi", dir: initialAgentDir } };
await mkdir(initialAgentDir, { recursive: true });
await installConfiguredPiWebPackage(updatedAgentDir);
const initialStatus = await app.inject({ method: "GET", url: "/api/pi-web/status" });
expect(initialStatus.statusCode).toBe(200);
const updateResponse = await app.inject({
method: "PUT",
url: "/api/config",
payload: { config: { agent: { command: "pi", dir: updatedAgentDir } } },
});
expect(updateResponse.statusCode).toBe(200);
const refreshedStatus = await app.inject({ method: "GET", url: "/api/pi-web/status" });
expect(refreshedStatus.statusCode).toBe(200);
expect(refreshedStatus.json<PiWebStatusResponse>().components.web.installation).toMatchObject({ kind: "pi-package", source: process.cwd(), scope: "user" });
} finally {
restoreEnv("PI_WEB_SKIP_VERSION_CHECK", originalSkipVersionCheck);
}
});
it("serves supported workspace images as previews", async () => { it("serves supported workspace images as previews", async () => {
const addResponse = await app.inject({ const addResponse = await app.inject({
method: "POST", method: "POST",
@@ -920,6 +949,16 @@ function fakeSessionDaemon(): SessionProxyDaemon {
}; };
} }
async function installConfiguredPiWebPackage(agentDir: string): Promise<void> {
await mkdir(agentDir, { recursive: true });
await writeFile(join(agentDir, "settings.json"), `${JSON.stringify({ packages: [process.cwd()] }, null, 2)}\n`, "utf8");
}
function restoreEnv(key: string, value: string | undefined): void {
if (value === undefined) Reflect.deleteProperty(process.env, key);
else process.env[key] = value;
}
function fakeRemoteClient(overrides: Partial<MachineClient>): MachineClient { function fakeRemoteClient(overrides: Partial<MachineClient>): MachineClient {
return { return {
request: () => Promise.resolve({ statusCode: 200, headers: {}, body: Readable.from([]) }), request: () => Promise.resolve({ statusCode: 200, headers: {}, body: Readable.from([]) }),
+35 -7
View File
@@ -20,9 +20,9 @@ import { registerTerminalProxyRoutes } from "./terminalProxyRoutes.js";
import { registerWorkspaceDeletionRoutes } from "./workspaces/workspaceDeletionRoutes.js"; import { registerWorkspaceDeletionRoutes } from "./workspaces/workspaceDeletionRoutes.js";
import { createFilePiWebConfigService, registerConfigRoutes, type PiWebConfigService } from "./configRoutes.js"; import { createFilePiWebConfigService, registerConfigRoutes, type PiWebConfigService } from "./configRoutes.js";
import { PiWebPluginService } from "./piWebPluginService.js"; import { PiWebPluginService } from "./piWebPluginService.js";
import { createPiWebStatusCache } from "./piWebStatusCache.js"; import { createPiWebStatusCache, type PiWebStatusCache } from "./piWebStatusCache.js";
import { getPiWebRuntime, getPiWebStatus, getPiWebVersionStatus } from "./piWebStatus.js"; import { getPiWebRuntime, getPiWebStatus, getPiWebVersionStatus } from "./piWebStatus.js";
import { effectiveAgentConfig, effectivePiWebConfig } from "../config.js"; import { effectiveAgentConfig, type EffectivePiWebAgentConfig } from "../config.js";
import { MachineService } from "./machines/machineService.js"; import { MachineService } from "./machines/machineService.js";
import { registerMachineRoutes } from "./machines/machineRoutes.js"; import { registerMachineRoutes } from "./machines/machineRoutes.js";
import { registerMachineProxyRoutes } from "./machines/machineProxyRoutes.js"; import { registerMachineProxyRoutes } from "./machines/machineProxyRoutes.js";
@@ -116,17 +116,42 @@ function registerLocalFileSuggestionRoutes(app: FastifyInstance, projects: Proje
}); });
} }
async function readEffectiveConfig(config: Pick<PiWebConfigService, "read">) {
return (await config.read()).effectiveConfig;
}
async function readEffectiveAgentConfig(config: Pick<PiWebConfigService, "read">): Promise<EffectivePiWebAgentConfig> {
return effectiveAgentConfig(process.env, await readEffectiveConfig(config));
}
function invalidatePiWebStatusOnWrite(config: PiWebConfigService, statusCache: Pick<PiWebStatusCache, "invalidate">): PiWebConfigService {
return {
read: () => config.read(),
write: async (nextConfig) => {
const response = await config.write(nextConfig);
statusCache.invalidate();
return response;
},
};
}
export async function buildApp(deps: AppDependencies = {}): Promise<FastifyInstance> { export async function buildApp(deps: AppDependencies = {}): Promise<FastifyInstance> {
const app = Fastify({ logger: deps.logger ?? true, ...(deps.bodyLimit === undefined ? {} : { bodyLimit: deps.bodyLimit }) }); const app = Fastify({ logger: deps.logger ?? true, ...(deps.bodyLimit === undefined ? {} : { bodyLimit: deps.bodyLimit }) });
await app.register(fastifyWebsocket); await app.register(fastifyWebsocket);
const projects = deps.projects ?? new ProjectService(new ProjectStore()); const projects = deps.projects ?? new ProjectService(new ProjectStore());
const workspaces = deps.workspaces ?? new WorkspaceService(); const workspaces = deps.workspaces ?? new WorkspaceService();
const agent = effectiveAgentConfig(process.env, effectivePiWebConfig().config);
const piWebPlugins = deps.piWebPlugins ?? new PiWebPluginService({ agentDir: agent.dir });
const configService = deps.config ?? createFilePiWebConfigService(); const configService = deps.config ?? createFilePiWebConfigService();
const readConfig = () => readEffectiveConfig(configService);
const readAgentConfig = () => readEffectiveAgentConfig(configService);
const piWebPlugins = deps.piWebPlugins ?? new PiWebPluginService({
configProvider: readConfig,
});
const sessionDaemon = deps.sessionDaemon ?? new SessionDaemonClient(); const sessionDaemon = deps.sessionDaemon ?? new SessionDaemonClient();
const piWebStatusCache = createPiWebStatusCache(() => getPiWebStatus(sessionDaemon, { agentCommand: agent.command, agentDir: agent.dir }), { const piWebStatusCache = createPiWebStatusCache(async () => {
const agent = await readAgentConfig();
return getPiWebStatus(sessionDaemon, { agentCommand: agent.command, agentDir: agent.dir });
}, {
onError: (error) => { app.log.warn({ err: error }, "failed to refresh PI WEB status cache"); }, onError: (error) => { app.log.warn({ err: error }, "failed to refresh PI WEB status cache"); },
}); });
const machines = deps.machines ?? new MachineService(undefined, { const machines = deps.machines ?? new MachineService(undefined, {
@@ -144,10 +169,13 @@ export async function buildApp(deps: AppDependencies = {}): Promise<FastifyInsta
}); });
app.get("/api/pi-web/status", async () => piWebStatusCache.get()); app.get("/api/pi-web/status", async () => piWebStatusCache.get());
app.get("/api/pi-web/version", async () => getPiWebVersionStatus(sessionDaemon, { agentCommand: agent.command, agentDir: agent.dir })); app.get("/api/pi-web/version", async () => {
const agent = await readAgentConfig();
return getPiWebVersionStatus(sessionDaemon, { agentCommand: agent.command, agentDir: agent.dir });
});
app.get("/api/pi-web/runtime", async () => getPiWebRuntime(sessionDaemon)); app.get("/api/pi-web/runtime", async () => getPiWebRuntime(sessionDaemon));
app.get("/api/plugins", async () => piWebPlugins.plugins()); app.get("/api/plugins", async () => piWebPlugins.plugins());
registerConfigRoutes(app, configService); registerConfigRoutes(app, invalidatePiWebStatusOnWrite(configService, piWebStatusCache));
registerMachineRoutes(app, machines); registerMachineRoutes(app, machines);
registerMachinePluginProxyRoutes(app, machines); registerMachinePluginProxyRoutes(app, machines);
+2 -2
View File
@@ -37,11 +37,11 @@ describe("config routes", () => {
const response = await app.inject({ const response = await app.inject({
method: "PUT", method: "PUT",
url: "/api/config", url: "/api/config",
payload: { config: { host: "0.0.0.0", port: 9000, allowedHosts: true, spawnSessions: true, subsessions: true, agent: { command: "omp", dir: "~/.omp/agent" }, shortcuts: { "core:view.chat": "mod+1", "core:session.stop": null }, plugins: { info: { enabled: false, settings: { note: "hidden" } } }, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: "uploads\\manual" }, maxUploadBytes: 1234 } }, payload: { config: { host: "0.0.0.0", port: 9000, allowedHosts: true, spawnSessions: true, subsessions: true, agent: { command: "agent-lab", dir: "~/agent-profiles/lab" }, shortcuts: { "core:view.chat": "mod+1", "core:session.stop": null }, plugins: { info: { enabled: false, settings: { note: "hidden" } } }, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: "uploads\\manual" }, maxUploadBytes: 1234 } },
}); });
expect(response.statusCode).toBe(200); expect(response.statusCode).toBe(200);
expect(savedConfig).toEqual({ host: "0.0.0.0", port: 9000, allowedHosts: true, spawnSessions: true, subsessions: true, agent: { command: "omp", dir: "~/.omp/agent" }, shortcuts: { "core:view.chat": "mod+1", "core:session.stop": null }, plugins: { info: { enabled: false, settings: { note: "hidden" } } }, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: "uploads/manual" }, maxUploadBytes: 1234 }); expect(savedConfig).toEqual({ host: "0.0.0.0", port: 9000, allowedHosts: true, spawnSessions: true, subsessions: true, agent: { command: "agent-lab", dir: "~/agent-profiles/lab" }, shortcuts: { "core:view.chat": "mod+1", "core:session.stop": null }, plugins: { info: { enabled: false, settings: { note: "hidden" } } }, pathAccess: { allowedPaths: ["/tmp"] }, uploads: { defaultFolder: "uploads/manual" }, maxUploadBytes: 1234 });
expect(response.json<PiWebConfigResponse>().config).toEqual(savedConfig); expect(response.json<PiWebConfigResponse>().config).toEqual(savedConfig);
}); });
+6 -7
View File
@@ -1,5 +1,5 @@
import type { FastifyInstance } from "fastify"; import type { FastifyInstance } from "fastify";
import { effectiveAgentConfig, effectivePiWebConfig, hasAgentDirEnvOverride, hasAgentSessionDirEnvOverride, loadPiWebConfig, parseUploadsConfig, savePiWebConfig, type LoadOptions, type PiWebConfig } from "../config.js"; import { hasAgentDirEnvOverride, hasAgentSessionDirEnvOverride, loadPiWebConfig, parseUploadsConfig, resolveEffectivePiWebConfig, savePiWebConfig, type LoadOptions, type PiWebConfig } from "../config.js";
import type { PiWebConfigEnvOverrides, PiWebConfigResponse, PiWebConfigValues } from "../shared/apiTypes.js"; import type { PiWebConfigEnvOverrides, PiWebConfigResponse, PiWebConfigValues } from "../shared/apiTypes.js";
import { isPiWebPluginId } from "../shared/pluginIds.js"; import { isPiWebPluginId } from "../shared/pluginIds.js";
@@ -20,14 +20,14 @@ export function createFilePiWebConfigService(options: LoadOptions = {}): PiWebCo
export function currentPiWebConfigResponse(options: LoadOptions = {}): PiWebConfigResponse { export function currentPiWebConfigResponse(options: LoadOptions = {}): PiWebConfigResponse {
const loaded = loadPiWebConfig(options); const loaded = loadPiWebConfig(options);
const effective = effectivePiWebConfig(options); const effective = resolveEffectivePiWebConfig(loaded, options);
const env = options.env ?? process.env; const env = options.env ?? process.env;
return { return {
path: loaded.path, path: loaded.path,
exists: loaded.exists, exists: loaded.exists,
config: loaded.config, config: loaded.config,
effectiveConfig: effective.config, effectiveConfig: effective.config,
envOverrides: piWebConfigEnvOverrides(env, loaded.config), envOverrides: piWebConfigEnvOverrides(env),
}; };
} }
@@ -167,8 +167,7 @@ function parsePluginsRequest(value: unknown): NonNullable<PiWebConfig["plugins"]
})); }));
} }
function piWebConfigEnvOverrides(env: NodeJS.ProcessEnv, config: PiWebConfig = {}): PiWebConfigEnvOverrides { function piWebConfigEnvOverrides(env: NodeJS.ProcessEnv): PiWebConfigEnvOverrides {
const agent = effectiveAgentConfig(env, config);
return { return {
host: isEnvSet(env["PI_WEB_HOST"]), host: isEnvSet(env["PI_WEB_HOST"]),
port: isEnvSet(env["PI_WEB_PORT"]) || isEnvSet(env["PORT"]), port: isEnvSet(env["PI_WEB_PORT"]) || isEnvSet(env["PORT"]),
@@ -176,8 +175,8 @@ function piWebConfigEnvOverrides(env: NodeJS.ProcessEnv, config: PiWebConfig = {
spawnSessions: isEnvSet(env["PI_WEB_SPAWN_SESSIONS"]), spawnSessions: isEnvSet(env["PI_WEB_SPAWN_SESSIONS"]),
subsessions: isEnvSet(env["PI_WEB_SUBSESSIONS"]), subsessions: isEnvSet(env["PI_WEB_SUBSESSIONS"]),
agentCommand: isEnvSet(env["PI_WEB_AGENT_COMMAND"]), agentCommand: isEnvSet(env["PI_WEB_AGENT_COMMAND"]),
agentDir: hasAgentDirEnvOverride(env, agent.command), agentDir: hasAgentDirEnvOverride(env),
agentSessionDir: hasAgentSessionDirEnvOverride(env, agent.command), agentSessionDir: hasAgentSessionDirEnvOverride(env),
}; };
} }
+21
View File
@@ -66,6 +66,27 @@ describe("PiWebPluginService", () => {
expect(manifest.plugins[0]?.module).toMatch(/^\/pi-web-plugins\/review\/dist\/review\.js\?v=\d+$/u); expect(manifest.plugins[0]?.module).toMatch(/^\/pi-web-plugins\/review\/dist\/review\.js\?v=\d+$/u);
}); });
it("uses the current config provider for Pi package plugin discovery", async () => {
const packageDir = join(tempDir, "pkg");
const initialAgentDir = join(tempDir, "initial-agent");
const updatedAgentDir = join(tempDir, "updated-agent");
let currentConfig = { agent: { dir: initialAgentDir } };
await writePlugin(packageDir, {
packageJson: { piWeb: { plugins: [{ id: "agent-package", module: "dist/plugin.js" }] } },
files: { "dist/plugin.js": "export default {};" },
});
await mkdir(initialAgentDir, { recursive: true });
await mkdir(updatedAgentDir, { recursive: true });
await writeFile(join(updatedAgentDir, "settings.json"), `${JSON.stringify({ packages: [packageDir] }, null, 2)}\n`, "utf8");
const service = new PiWebPluginService({ roots: [], cwd: tempDir, configProvider: () => currentConfig });
await expect(service.manifest()).resolves.toEqual({ plugins: [] });
currentConfig = { agent: { dir: updatedAgentDir } };
await expect(service.manifest()).resolves.toMatchObject({ plugins: [{ id: "agent-package", source: packageDir, scope: "user" }] });
});
it("discovers source checkout plugin packages without symlinks", async () => { it("discovers source checkout plugin packages without symlinks", async () => {
await mkdir(join(tempDir, "src", "server"), { recursive: true }); await mkdir(join(tempDir, "src", "server"), { recursive: true });
await writeFile(join(tempDir, "src", "server", "index.ts"), "export {};\n"); await writeFile(join(tempDir, "src", "server", "index.ts"), "export {};\n");
+46 -14
View File
@@ -2,8 +2,8 @@ import { existsSync } from "node:fs";
import { readdir, readFile, realpath, stat } from "node:fs/promises"; import { readdir, readFile, realpath, stat } from "node:fs/promises";
import { dirname, join, relative, resolve, sep } from "node:path"; import { dirname, join, relative, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url"; import { fileURLToPath } from "node:url";
import { DefaultPackageManager, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent"; import { DefaultPackageManager, SettingsManager } from "@earendil-works/pi-coding-agent";
import { loadPiWebConfig, piWebDataDir, type PiWebConfig } from "../config.js"; import { effectiveAgentConfig, loadPiWebConfig, piWebDataDir, type PiWebConfig } from "../config.js";
import type { PiWebPluginInfo, PiWebPluginsResponse, PiWebPluginScope } from "../shared/apiTypes.js"; import type { PiWebPluginInfo, PiWebPluginsResponse, PiWebPluginScope } from "../shared/apiTypes.js";
import { isPiWebPluginId } from "../shared/pluginIds.js"; import { isPiWebPluginId } from "../shared/pluginIds.js";
@@ -46,8 +46,9 @@ interface PiWebPluginServiceOptions {
roots?: LocalPluginRoot[]; roots?: LocalPluginRoot[];
cwd?: string; cwd?: string;
agentDir?: string; agentDir?: string;
agentDirProvider?: () => string | Promise<string>;
packageProvider?: PiPackageProvider | false; packageProvider?: PiPackageProvider | false;
configProvider?: () => PiWebConfig; configProvider?: () => PiWebConfig | Promise<PiWebConfig>;
} }
interface LocalPluginRoot { interface LocalPluginRoot {
@@ -71,11 +72,12 @@ type ArraylessPluginRecord = Omit<PluginRecord, "source" | "scope">;
export class DefaultPiPackageProvider implements PiPackageProvider { export class DefaultPiPackageProvider implements PiPackageProvider {
private readonly packageManager: DefaultPackageManager; private readonly packageManager: DefaultPackageManager;
constructor(cwd = process.cwd(), agentDir = getAgentDir()) { constructor(cwd = process.cwd(), agentDir?: string) {
const resolvedAgentDir = agentDir ?? defaultAgentDirForCwd(cwd);
this.packageManager = new DefaultPackageManager({ this.packageManager = new DefaultPackageManager({
cwd, cwd,
agentDir, agentDir: resolvedAgentDir,
settingsManager: SettingsManager.create(cwd, agentDir), settingsManager: SettingsManager.create(cwd, resolvedAgentDir),
}); });
} }
@@ -88,16 +90,32 @@ export class DefaultPiPackageProvider implements PiPackageProvider {
} }
} }
function defaultAgentDirForCwd(cwd: string): string {
return effectiveAgentConfig(process.env, loadPiWebConfig({ cwd }).config, cwd).dir;
}
export class PiWebPluginService { export class PiWebPluginService {
private readonly cwd: string;
private readonly roots: LocalPluginRoot[]; private readonly roots: LocalPluginRoot[];
private readonly packageProvider: PiPackageProvider | undefined; private readonly agentDir: string | undefined;
private readonly configProvider: () => PiWebConfig; private readonly agentDirProvider: (() => string | Promise<string>) | undefined;
private readonly packageProviderForAgentDir: ((agentDir: string) => PiPackageProvider) | undefined;
private readonly configProvider: () => PiWebConfig | Promise<PiWebConfig>;
constructor(options: PiWebPluginServiceOptions = {}) { constructor(options: PiWebPluginServiceOptions = {}) {
const cwd = options.cwd ?? process.cwd(); const cwd = options.cwd ?? process.cwd();
const agentDir = options.agentDir ?? getAgentDir(); this.cwd = cwd;
this.roots = options.roots ?? defaultPluginRoots(cwd); this.roots = options.roots ?? defaultPluginRoots(cwd);
this.packageProvider = options.packageProvider === false ? undefined : options.packageProvider ?? new DefaultPiPackageProvider(cwd, agentDir); this.agentDir = options.agentDir;
this.agentDirProvider = options.agentDirProvider;
const packageProvider = options.packageProvider;
if (packageProvider === false) {
this.packageProviderForAgentDir = undefined;
} else if (packageProvider !== undefined) {
this.packageProviderForAgentDir = () => packageProvider;
} else {
this.packageProviderForAgentDir = (agentDir) => new DefaultPiPackageProvider(cwd, agentDir);
}
this.configProvider = options.configProvider ?? (() => loadPiWebConfig({ cwd }).config); this.configProvider = options.configProvider ?? (() => loadPiWebConfig({ cwd }).config);
} }
@@ -110,7 +128,8 @@ export class PiWebPluginService {
} }
async plugins(): Promise<PiWebPluginsResponse> { async plugins(): Promise<PiWebPluginsResponse> {
const [plugins, config] = await Promise.all([this.discoverPlugins(), Promise.resolve(this.configProvider())]); const config = await this.configProvider();
const plugins = await this.discoverPlugins(config);
return { plugins: plugins.map((plugin) => this.pluginInfo(plugin, config)) }; return { plugins: plugins.map((plugin) => this.pluginInfo(plugin, config)) };
} }
@@ -143,15 +162,28 @@ export class PiWebPluginService {
}; };
} }
private async discoverPlugins(): Promise<PluginRecord[]> { private async discoverPlugins(config?: PiWebConfig): Promise<PluginRecord[]> {
const records = new Map<string, PluginRecord>(); const records = new Map<string, PluginRecord>();
for (const plugin of await this.discoverLocalPlugins()) addUnique(records, plugin); for (const plugin of await this.discoverLocalPlugins()) addUnique(records, plugin);
if (this.packageProvider !== undefined) { const packageProvider = await this.packageProvider(config);
for (const plugin of await this.discoverPiPackagePlugins(this.packageProvider)) addUnique(records, plugin); if (packageProvider !== undefined) {
for (const plugin of await this.discoverPiPackagePlugins(packageProvider)) addUnique(records, plugin);
} }
return [...records.values()].sort((left, right) => left.id.localeCompare(right.id)); return [...records.values()].sort((left, right) => left.id.localeCompare(right.id));
} }
private async packageProvider(config?: PiWebConfig): Promise<PiPackageProvider | undefined> {
if (this.packageProviderForAgentDir === undefined) return undefined;
return this.packageProviderForAgentDir(await this.currentAgentDir(config));
}
private async currentAgentDir(config?: PiWebConfig): Promise<string> {
if (this.agentDirProvider !== undefined) return await this.agentDirProvider();
if (this.agentDir !== undefined) return this.agentDir;
const currentConfig = config ?? await this.configProvider();
return effectiveAgentConfig(process.env, currentConfig, this.cwd).dir;
}
private async discoverLocalPlugins(): Promise<PluginRecord[]> { private async discoverLocalPlugins(): Promise<PluginRecord[]> {
const plugins: PluginRecord[] = []; const plugins: PluginRecord[] = [];
for (const root of this.roots) plugins.push(...await discoverLocalRoot(root)); for (const root of this.roots) plugins.push(...await discoverLocalRoot(root));
+4 -4
View File
@@ -52,7 +52,7 @@ describe("PI WEB status", () => {
capabilities: [], capabilities: [],
}); });
const status = await getPiWebVersionStatus(daemon, { agentCommand: "omp", agentDir }); const status = await getPiWebVersionStatus(daemon, { agentCommand: "alt-agent", agentDir });
expect(status.components.sessiond.installation).toMatchObject({ kind: "pi-package", source: process.cwd(), scope: "user" }); expect(status.components.sessiond.installation).toMatchObject({ kind: "pi-package", source: process.cwd(), scope: "user" });
} finally { } finally {
@@ -85,12 +85,12 @@ describe("PI WEB status", () => {
{ kind: "pi-package", source: "npm:@jmfederico/pi-web", scope: "user", path: "/tmp/pi-web" }, { kind: "pi-package", source: "npm:@jmfederico/pi-web", scope: "user", path: "/tmp/pi-web" },
"pi-web restart", "pi-web restart",
{ {
agentCommand: "/tmp/agent's/omp", agentCommand: "/tmp/agent's/alt-agent",
hasCommand: (command) => Promise.resolve(command === "/tmp/agent's/omp"), hasCommand: (command) => Promise.resolve(command === "/tmp/agent's/alt-agent"),
}, },
); );
expect(updateCommand).toBe("'/tmp/agent'\\''s/omp' update 'npm:@jmfederico/pi-web' && pi-web restart"); expect(updateCommand).toBe("'/tmp/agent'\\''s/alt-agent' update 'npm:@jmfederico/pi-web' && pi-web restart");
}); });
it("suggests native systemd commands for local development services", async () => { it("suggests native systemd commands for local development services", async () => {
+4
View File
@@ -11,6 +11,7 @@ export interface PiWebStatusCacheOptions {
export interface PiWebStatusCache { export interface PiWebStatusCache {
get(): Promise<PiWebStatusResponse>; get(): Promise<PiWebStatusResponse>;
refresh(): Promise<PiWebStatusResponse>; refresh(): Promise<PiWebStatusResponse>;
invalidate(): void;
} }
export function createPiWebStatusCache(load: () => Promise<PiWebStatusResponse>, options: PiWebStatusCacheOptions = {}): PiWebStatusCache { export function createPiWebStatusCache(load: () => Promise<PiWebStatusResponse>, options: PiWebStatusCacheOptions = {}): PiWebStatusCache {
@@ -42,5 +43,8 @@ export function createPiWebStatusCache(load: () => Promise<PiWebStatusResponse>,
return refresh(); return refresh();
}, },
refresh, refresh,
invalidate(): void {
cached = undefined;
},
}; };
} }
+2 -2
View File
@@ -44,9 +44,9 @@ describe("AuthService", () => {
const agentDir = await tempAgentDir(); const agentDir = await tempAgentDir();
const auth = new AuthService({ agentDir }); const auth = new AuthService({ agentDir });
auth.saveApiKey("anthropic", "sk-omp"); auth.saveApiKey("anthropic", "sk-test");
await expect(readFile(join(agentDir, "auth.json"), "utf8")).resolves.toContain("sk-omp"); await expect(readFile(join(agentDir, "auth.json"), "utf8")).resolves.toContain("sk-test");
auth.dispose(); auth.dispose();
}); });
}); });
@@ -60,12 +60,12 @@ describe("SessionDirResolver", () => {
expect(resolver.resolve(cwd)).toMatchObject({ source: "env", sessionDir: envDir, usesConfiguredSessionDir: true }); expect(resolver.resolve(cwd)).toMatchObject({ source: "env", sessionDir: envDir, usesConfiguredSessionDir: true });
}); });
it("uses OMP sessionDir environment overrides before settings", async () => { it("uses PI WEB sessionDir environment overrides before settings", async () => {
const envDir = join(tempDir, "omp-env-sessions"); const envDir = join(tempDir, "pi-web-env-sessions");
await mkdir(agentDir, { recursive: true }); await mkdir(agentDir, { recursive: true });
await writeFile(join(agentDir, "settings.json"), `${JSON.stringify({ sessionDir: join(tempDir, "settings-sessions") }, null, 2)}\n`, "utf8"); await writeFile(join(agentDir, "settings.json"), `${JSON.stringify({ sessionDir: join(tempDir, "settings-sessions") }, null, 2)}\n`, "utf8");
const resolver = new SessionDirResolver({ agentDir, env: { OMP_CODING_AGENT_SESSION_DIR: envDir }, sessionDirEnvKeys: ["PI_WEB_AGENT_SESSION_DIR", "OMP_CODING_AGENT_SESSION_DIR", "PI_CODING_AGENT_SESSION_DIR"] }); const resolver = new SessionDirResolver({ agentDir, env: { PI_WEB_AGENT_SESSION_DIR: envDir } });
expect(resolver.resolve(cwd)).toMatchObject({ source: "env", sessionDir: envDir, usesConfiguredSessionDir: true }); expect(resolver.resolve(cwd)).toMatchObject({ source: "env", sessionDir: envDir, usesConfiguredSessionDir: true });
}); });
@@ -92,14 +92,13 @@ describe("Pi session manager gateway", () => {
await expect(gateway.listAll()).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: "default-session", cwd }), expect.objectContaining({ id: "env-session", cwd })])); await expect(gateway.listAll()).resolves.toEqual(expect.arrayContaining([expect.objectContaining({ id: "default-session", cwd }), expect.objectContaining({ id: "env-session", cwd })]));
}); });
it("includes command-specific env session directories in global listing", async () => { it("includes generic env session directories in global listing", async () => {
for (const envKey of ["PI_WEB_AGENT_SESSION_DIR", "OMP_CODING_AGENT_SESSION_DIR"]) { for (const envKey of ["PI_WEB_AGENT_SESSION_DIR", "PI_CODING_AGENT_SESSION_DIR"]) {
const envSessionDir = join(tempDir, `${envKey.toLowerCase()}-sessions`); const envSessionDir = join(tempDir, `${envKey.toLowerCase()}-sessions`);
await writeSessionFile(envSessionDir, `${envKey.toLowerCase()}-session`, cwd); await writeSessionFile(envSessionDir, `${envKey.toLowerCase()}-session`, cwd);
const gateway = createPiSessionManagerGateway({ const gateway = createPiSessionManagerGateway({
agentDir, agentDir,
env: { [envKey]: envSessionDir }, env: { [envKey]: envSessionDir },
sessionDirEnvKeys: ["PI_WEB_AGENT_SESSION_DIR", "OMP_CODING_AGENT_SESSION_DIR", "PI_CODING_AGENT_SESSION_DIR"],
}); });
if (gateway.listAll === undefined) throw new Error("Expected legacy listing support"); if (gateway.listAll === undefined) throw new Error("Expected legacy listing support");
@@ -2,12 +2,11 @@ import type { Dirent } from "node:fs";
import { readdir } from "node:fs/promises"; import { readdir } from "node:fs/promises";
import { homedir } from "node:os"; import { homedir } from "node:os";
import { dirname, isAbsolute, join, resolve } from "node:path"; import { dirname, isAbsolute, join, resolve } from "node:path";
import { getAgentDir, SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent"; import { SessionManager, SettingsManager } from "@earendil-works/pi-coding-agent";
import { agentSessionDirEnvKeys, effectiveAgentConfig } from "../../config.js";
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js"; import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
import type { PiSessionListEntry, PiSessionManager, PiSessionManagerGateway } from "./piSessionService.js"; import type { PiSessionListEntry, PiSessionManager, PiSessionManagerGateway } from "./piSessionService.js";
export const PI_SESSION_DIR_ENV = "PI_CODING_AGENT_SESSION_DIR";
type SessionDirSource = "env" | "settings" | "pi-default"; type SessionDirSource = "env" | "settings" | "pi-default";
export interface SessionDirResolution { export interface SessionDirResolution {
@@ -28,9 +27,9 @@ export class SessionDirResolver {
private readonly sessionDirEnvKeys: readonly string[]; private readonly sessionDirEnvKeys: readonly string[];
constructor(options: SessionDirResolverOptions = {}) { constructor(options: SessionDirResolverOptions = {}) {
this.agentDir = options.agentDir ?? getAgentDir(); this.agentDir = options.agentDir ?? effectiveAgentConfig().dir;
this.env = options.env ?? process.env; this.env = options.env ?? process.env;
this.sessionDirEnvKeys = options.sessionDirEnvKeys ?? [PI_SESSION_DIR_ENV]; this.sessionDirEnvKeys = options.sessionDirEnvKeys ?? agentSessionDirEnvKeys();
} }
defaultSessionsRoot(): string { defaultSessionsRoot(): string {
@@ -131,11 +130,11 @@ function uniqueSessionsByPath(sessions: readonly PiSessionListEntry[]): PiSessio
return [...byPath.values()].sort((a, b) => b.modified.getTime() - a.modified.getTime()); return [...byPath.values()].sort((a, b) => b.modified.getTime() - a.modified.getTime());
} }
export function defaultPiSessionsRoot(agentDir = getAgentDir()): string { export function defaultPiSessionsRoot(agentDir = effectiveAgentConfig().dir): string {
return join(agentDir, "sessions"); return join(agentDir, "sessions");
} }
export function defaultPiSessionDir(cwd: string, agentDir = getAgentDir()): string { export function defaultPiSessionDir(cwd: string, agentDir = effectiveAgentConfig().dir): string {
return sessionDirInDefaultPiStore(defaultPiSessionsRoot(agentDir), cwd); return sessionDirInDefaultPiStore(defaultPiSessionsRoot(agentDir), cwd);
} }
+2 -2
View File
@@ -7,7 +7,6 @@ import {
createAgentSessionServices, createAgentSessionServices,
createEditToolDefinition, createEditToolDefinition,
defineTool, defineTool,
getAgentDir,
ModelRegistry, ModelRegistry,
SessionManager, SessionManager,
type CreateAgentSessionRuntimeFactory, type CreateAgentSessionRuntimeFactory,
@@ -25,6 +24,7 @@ import { createModelRegistryForAgentDir, type AuthChange } from "./authService.j
import { fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js"; import { fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
import { computeEditPreview, type EditPreviewResult } from "./editPreview.js"; import { computeEditPreview, type EditPreviewResult } from "./editPreview.js";
import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js"; import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
import { effectiveAgentConfig } from "../../config.js";
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js"; import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
import { parsePromptAttachments } from "../../shared/promptAttachments.js"; import { parsePromptAttachments } from "../../shared/promptAttachments.js";
import type { SavedPromptAttachment } from "../../shared/apiTypes.js"; import type { SavedPromptAttachment } from "../../shared/apiTypes.js";
@@ -338,7 +338,7 @@ export class PiSessionService implements SessionRouteService {
constructor(private readonly events: SessionEventHub, deps: PiSessionServiceDependencies = {}) { constructor(private readonly events: SessionEventHub, deps: PiSessionServiceDependencies = {}) {
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore(); this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
this.agentDir = deps.agentDir ?? getAgentDir(); this.agentDir = deps.agentDir ?? effectiveAgentConfig().dir;
this.sessionManager = deps.sessionManager ?? createPiSessionManagerGateway({ agentDir: this.agentDir }); this.sessionManager = deps.sessionManager ?? createPiSessionManagerGateway({ agentDir: this.agentDir });
this.modelRegistry = deps.modelRegistry ?? createModelRegistryForAgentDir(this.agentDir); this.modelRegistry = deps.modelRegistry ?? createModelRegistryForAgentDir(this.agentDir);
this.spawnTargets = deps.spawnTargets; this.spawnTargets = deps.spawnTargets;
+1 -1
View File
@@ -89,7 +89,7 @@ export interface PiWebConfigValues {
* while the capability stabilizes. Requires spawnSessions to be enabled. * while the capability stabilizes. Requires spawnSessions to be enabled.
*/ */
subsessions?: boolean; subsessions?: boolean;
/** Agent runtime state used by the session daemon (Pi by default; OMP compatible). */ /** Agent runtime command/state used by PI WEB and the session daemon (Pi by default). */
agent?: PiWebAgentConfig; agent?: PiWebAgentConfig;
} }