Archived
feat: generalize agent runtime config
This commit is contained in:
@@ -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.
|
||||
@@ -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
@@ -131,8 +131,7 @@
|
||||
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_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>OMP_CODING_AGENT_SESSION_DIR</code>, <code>PI_WEB_SPAWN_SESSIONS</code>, and
|
||||
<code>PI_CODING_AGENT_SESSION_DIR</code>, <code>PI_WEB_SPAWN_SESSIONS</code>, and
|
||||
<code>PI_WEB_SUBSESSIONS</code>.
|
||||
</p>
|
||||
<ul>
|
||||
@@ -165,8 +164,8 @@
|
||||
},
|
||||
"maxUploadBytes": 67108864,
|
||||
"agent": {
|
||||
"command": "omp",
|
||||
"dir": "~/.omp/agent"
|
||||
"command": "pi",
|
||||
"dir": "~/agent-profiles/research"
|
||||
},
|
||||
"spawnSessions": true,
|
||||
"subsessions": false,
|
||||
@@ -281,7 +280,7 @@
|
||||
<tr>
|
||||
<td>Agent state directory</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>Not supported locally</td>
|
||||
<td>Restart session daemon; affects auth, models, settings, and sessions</td>
|
||||
@@ -394,7 +393,7 @@
|
||||
<tr>
|
||||
<td>Agent session storage directory</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>Not supported locally</td>
|
||||
<td>Restart session daemon; env-only session storage override</td>
|
||||
@@ -402,7 +401,7 @@
|
||||
<tr>
|
||||
<td>Agent config directory</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>Not supported locally</td>
|
||||
<td>Restart services</td>
|
||||
@@ -448,34 +447,41 @@
|
||||
<h2>Agent runtime</h2>
|
||||
<p>
|
||||
<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>
|
||||
<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
|
||||
directory (<code>~/.pi/agent</code> for <code>pi</code>, <code>~/.omp/agent</code> for <code>omp</code>).
|
||||
model settings, settings, and session metadata. It defaults to <code>~/.pi/agent</code>. Set it to another
|
||||
Pi-compatible state directory when you want an isolated profile or an alternate compatible agent's data.
|
||||
</p>
|
||||
<div class="code-card">
|
||||
<pre><code>{
|
||||
"agent": {
|
||||
"command": "omp",
|
||||
"dir": "~/.omp/agent"
|
||||
"command": "pi",
|
||||
"dir": "~/agent-profiles/research"
|
||||
}
|
||||
}</code></pre>
|
||||
</div>
|
||||
<p>
|
||||
Environment variables take precedence over the config file. <code>PI_WEB_AGENT_COMMAND</code> selects the
|
||||
command, <code>PI_WEB_AGENT_DIR</code> sets the state directory for any command, and command-specific
|
||||
variables such as <code>OMP_CODING_AGENT_DIR</code> are honored when the selected command is <code>omp</code>.
|
||||
For example, an Oh My Pi profile can set <code>agent.command</code> to <code>omp</code> and
|
||||
<code>agent.dir</code> to <code>~/.omp/agent</code>.
|
||||
</p>
|
||||
<p>
|
||||
Session directory overrides are environment-only. Set <code>PI_WEB_AGENT_SESSION_DIR</code> or the selected
|
||||
command's session variable (for example <code>OMP_CODING_AGENT_SESSION_DIR</code>) when you need to override
|
||||
session storage separately from <code>agent.dir</code>.
|
||||
Environment variables take precedence over the config file. <code>PI_WEB_AGENT_COMMAND</code> selects the
|
||||
command, <code>PI_WEB_AGENT_DIR</code> sets the state directory for any command, and
|
||||
<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>
|
||||
<div class="callout warning">
|
||||
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>
|
||||
</section>
|
||||
|
||||
|
||||
+15
-13
@@ -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.
|
||||
|
||||
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:
|
||||
|
||||
@@ -51,8 +51,8 @@ Process restarts depend on the key:
|
||||
},
|
||||
"maxUploadBytes": 67108864,
|
||||
"agent": {
|
||||
"command": "omp",
|
||||
"dir": "~/.omp/agent"
|
||||
"command": "pi",
|
||||
"dir": "~/agent-profiles/research"
|
||||
},
|
||||
"spawnSessions": true,
|
||||
"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 |
|
||||
| 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 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 |
|
||||
| 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 |
|
||||
@@ -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 |
|
||||
| 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 |
|
||||
| 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 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 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` | 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 |
|
||||
|
||||
## Key details
|
||||
@@ -168,24 +168,26 @@ The per-request size limit is still controlled by `maxUploadBytes` / `PI_WEB_MAX
|
||||
|
||||
### 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
|
||||
{
|
||||
"agent": {
|
||||
"command": "omp",
|
||||
"dir": "~/.omp/agent"
|
||||
"command": "pi",
|
||||
"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
|
||||
|
||||
|
||||
@@ -7,14 +7,14 @@ 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, 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" } },
|
||||
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: "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 },
|
||||
})).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, 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" } },
|
||||
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: "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 },
|
||||
});
|
||||
});
|
||||
|
||||
@@ -54,7 +54,7 @@ export class SettingsSessiondPanel extends LitElement {
|
||||
?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>
|
||||
<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 class="field">
|
||||
<span class="field-heading">
|
||||
@@ -67,11 +67,11 @@ export class SettingsSessiondPanel extends LitElement {
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
.value=${config?.config.agent?.dir ?? ""}
|
||||
placeholder="~/.pi/agent or ~/.omp/agent"
|
||||
placeholder="~/.pi/agent or ~/agent-profiles/work"
|
||||
?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>
|
||||
<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 class="field">
|
||||
<span class="field-heading">
|
||||
|
||||
@@ -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, 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",
|
||||
port: 9000,
|
||||
allowedHosts: ["example.local", "192.168.1.20"],
|
||||
@@ -29,7 +29,7 @@ describe("settings config drafts", () => {
|
||||
pathAccess: { allowedPaths: ["/tmp", "~/SDKs"] },
|
||||
uploads: { defaultFolder: "manual/uploads" },
|
||||
maxUploadBytes: 1234,
|
||||
agent: { command: "omp", dir: "~/.omp/agent" },
|
||||
agent: { command: "agent-lab", dir: "~/agent-profiles/lab" },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+43
-25
@@ -2,7 +2,7 @@ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
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 configPath: string;
|
||||
@@ -55,43 +55,61 @@ describe("PI WEB config persistence", () => {
|
||||
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({
|
||||
command: "omp",
|
||||
dir: join(tempDir, ".home", ".omp", "agent"),
|
||||
sessionDirEnvKeys: ["PI_WEB_AGENT_SESSION_DIR", "OMP_CODING_AGENT_SESSION_DIR", "PI_CODING_AGENT_SESSION_DIR"],
|
||||
dir: join(tempDir, ".home", ".pi", "agent"),
|
||||
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({
|
||||
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({
|
||||
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", () => {
|
||||
expect(effectiveAgentConfig({
|
||||
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("does not generate command-specific session directory env keys", () => {
|
||||
const keys = ["PI_WEB_AGENT_SESSION_DIR", "PI_CODING_AGENT_SESSION_DIR"];
|
||||
|
||||
it("normalizes omp.exe to OMP environment keys", () => {
|
||||
expect(effectiveAgentConfig({
|
||||
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"],
|
||||
});
|
||||
expect(agentSessionDirEnvKeys()).toEqual(keys);
|
||||
expect(effectiveAgentConfig({ HOME: join(tempDir, ".home"), PI_WEB_AGENT_COMMAND: "omp" }).sessionDirEnvKeys).toEqual(keys);
|
||||
});
|
||||
|
||||
it("exposes the default upload folder in the effective config", () => {
|
||||
|
||||
+18
-31
@@ -49,26 +49,25 @@ export interface 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 commandDirEnv = commandAgentDirEnv(command);
|
||||
const configuredDir = env[PI_WEB_AGENT_DIR_ENV] ?? env[commandDirEnv] ?? config.agent?.dir ?? defaultAgentDirForCommand(command, env);
|
||||
const command = parseAgentCommand(envValue(env, PI_WEB_AGENT_COMMAND_ENV) ?? config.agent?.command ?? DEFAULT_AGENT_COMMAND, "agent.command", "environment");
|
||||
const configuredDir = envValue(env, PI_WEB_AGENT_DIR_ENV) ?? envValue(env, PI_CODING_AGENT_DIR_ENV) ?? config.agent?.dir ?? defaultAgentDir(env);
|
||||
return {
|
||||
command,
|
||||
dir: resolveAgentDirPath(configuredDir, env, cwd, "agent.dir", "environment"),
|
||||
sessionDirEnvKeys: agentSessionDirEnvKeys(command),
|
||||
sessionDirEnvKeys: agentSessionDirEnvKeys(),
|
||||
};
|
||||
}
|
||||
|
||||
export function agentSessionDirEnvKeys(command = DEFAULT_AGENT_COMMAND): string[] {
|
||||
return uniqueStrings([PI_WEB_AGENT_SESSION_DIR_ENV, commandSessionDirEnv(command), PI_CODING_AGENT_SESSION_DIR_ENV]);
|
||||
export function agentSessionDirEnvKeys(): string[] {
|
||||
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 {
|
||||
return isEnvSet(env[PI_WEB_AGENT_DIR_ENV]) || isEnvSet(env[commandAgentDirEnv(command)]);
|
||||
export function hasAgentDirEnvOverride(env: NodeJS.ProcessEnv): boolean {
|
||||
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 {
|
||||
return agentSessionDirEnvKeys(command).some((key) => isEnvSet(env[key]));
|
||||
export function hasAgentSessionDirEnvOverride(env: NodeJS.ProcessEnv): boolean {
|
||||
return agentSessionDirEnvKeys().some((key) => isEnvSet(env[key]));
|
||||
}
|
||||
|
||||
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 {
|
||||
const loaded = loadPiWebConfig(options);
|
||||
return resolveEffectivePiWebConfig(loadPiWebConfig(options), options);
|
||||
}
|
||||
|
||||
export function resolveEffectivePiWebConfig(loaded: LoadedPiWebConfig, options: LoadOptions = {}): LoadedPiWebConfig {
|
||||
const env = options.env ?? process.env;
|
||||
const host = env["PI_WEB_HOST"];
|
||||
const port = env["PI_WEB_PORT"] ?? env["PORT"];
|
||||
@@ -333,28 +335,13 @@ function expandHomePath(value: string, env: NodeJS.ProcessEnv): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
function defaultAgentDirForCommand(command: string, env: NodeJS.ProcessEnv): string {
|
||||
return expandHomePath(isOmpCommand(command) ? "~/.omp/agent" : "~/.pi/agent", env);
|
||||
function defaultAgentDir(env: NodeJS.ProcessEnv): string {
|
||||
return expandHomePath("~/.pi/agent", env);
|
||||
}
|
||||
|
||||
function commandAgentDirEnv(command: string): string {
|
||||
const prefix = agentEnvPrefix(command);
|
||||
return prefix === "PI" ? PI_CODING_AGENT_DIR_ENV : `${prefix}_CODING_AGENT_DIR`;
|
||||
}
|
||||
|
||||
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 envValue(env: NodeJS.ProcessEnv, key: string): string | undefined {
|
||||
const value = env[key];
|
||||
return value !== undefined && value !== "" ? value : undefined;
|
||||
}
|
||||
|
||||
function isEnvSet(value: string | undefined): boolean {
|
||||
|
||||
+40
-1
@@ -15,7 +15,7 @@ import type { SessionProxyDaemon } from "./sessiond/sessionProxyRoutes.js";
|
||||
import { PI_WEB_CAPABILITIES } from "../shared/capabilities.js";
|
||||
import { machineScopedPluginId } from "../shared/machinePluginIds.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";
|
||||
|
||||
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 () => {
|
||||
const addResponse = await app.inject({
|
||||
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 {
|
||||
return {
|
||||
request: () => Promise.resolve({ statusCode: 200, headers: {}, body: Readable.from([]) }),
|
||||
|
||||
+35
-7
@@ -20,9 +20,9 @@ import { registerTerminalProxyRoutes } from "./terminalProxyRoutes.js";
|
||||
import { registerWorkspaceDeletionRoutes } from "./workspaces/workspaceDeletionRoutes.js";
|
||||
import { createFilePiWebConfigService, registerConfigRoutes, type PiWebConfigService } from "./configRoutes.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 { effectiveAgentConfig, effectivePiWebConfig } from "../config.js";
|
||||
import { effectiveAgentConfig, type EffectivePiWebAgentConfig } from "../config.js";
|
||||
import { MachineService } from "./machines/machineService.js";
|
||||
import { registerMachineRoutes } from "./machines/machineRoutes.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> {
|
||||
const app = Fastify({ logger: deps.logger ?? true, ...(deps.bodyLimit === undefined ? {} : { bodyLimit: deps.bodyLimit }) });
|
||||
await app.register(fastifyWebsocket);
|
||||
|
||||
const projects = deps.projects ?? new ProjectService(new ProjectStore());
|
||||
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 readConfig = () => readEffectiveConfig(configService);
|
||||
const readAgentConfig = () => readEffectiveAgentConfig(configService);
|
||||
const piWebPlugins = deps.piWebPlugins ?? new PiWebPluginService({
|
||||
configProvider: readConfig,
|
||||
});
|
||||
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"); },
|
||||
});
|
||||
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/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/plugins", async () => piWebPlugins.plugins());
|
||||
registerConfigRoutes(app, configService);
|
||||
registerConfigRoutes(app, invalidatePiWebStatusOnWrite(configService, piWebStatusCache));
|
||||
|
||||
registerMachineRoutes(app, machines);
|
||||
registerMachinePluginProxyRoutes(app, machines);
|
||||
|
||||
@@ -37,11 +37,11 @@ describe("config routes", () => {
|
||||
const response = await app.inject({
|
||||
method: "PUT",
|
||||
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(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);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { isPiWebPluginId } from "../shared/pluginIds.js";
|
||||
|
||||
@@ -20,14 +20,14 @@ export function createFilePiWebConfigService(options: LoadOptions = {}): PiWebCo
|
||||
|
||||
export function currentPiWebConfigResponse(options: LoadOptions = {}): PiWebConfigResponse {
|
||||
const loaded = loadPiWebConfig(options);
|
||||
const effective = effectivePiWebConfig(options);
|
||||
const effective = resolveEffectivePiWebConfig(loaded, options);
|
||||
const env = options.env ?? process.env;
|
||||
return {
|
||||
path: loaded.path,
|
||||
exists: loaded.exists,
|
||||
config: loaded.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 {
|
||||
const agent = effectiveAgentConfig(env, config);
|
||||
function piWebConfigEnvOverrides(env: NodeJS.ProcessEnv): PiWebConfigEnvOverrides {
|
||||
return {
|
||||
host: isEnvSet(env["PI_WEB_HOST"]),
|
||||
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"]),
|
||||
subsessions: isEnvSet(env["PI_WEB_SUBSESSIONS"]),
|
||||
agentCommand: isEnvSet(env["PI_WEB_AGENT_COMMAND"]),
|
||||
agentDir: hasAgentDirEnvOverride(env, agent.command),
|
||||
agentSessionDir: hasAgentSessionDirEnvOverride(env, agent.command),
|
||||
agentDir: hasAgentDirEnvOverride(env),
|
||||
agentSessionDir: hasAgentSessionDirEnvOverride(env),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -66,6 +66,27 @@ describe("PiWebPluginService", () => {
|
||||
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 () => {
|
||||
await mkdir(join(tempDir, "src", "server"), { recursive: true });
|
||||
await writeFile(join(tempDir, "src", "server", "index.ts"), "export {};\n");
|
||||
|
||||
@@ -2,8 +2,8 @@ import { existsSync } from "node:fs";
|
||||
import { readdir, readFile, realpath, stat } from "node:fs/promises";
|
||||
import { dirname, join, relative, resolve, sep } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { DefaultPackageManager, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
|
||||
import { loadPiWebConfig, piWebDataDir, type PiWebConfig } from "../config.js";
|
||||
import { DefaultPackageManager, SettingsManager } from "@earendil-works/pi-coding-agent";
|
||||
import { effectiveAgentConfig, loadPiWebConfig, piWebDataDir, type PiWebConfig } from "../config.js";
|
||||
import type { PiWebPluginInfo, PiWebPluginsResponse, PiWebPluginScope } from "../shared/apiTypes.js";
|
||||
import { isPiWebPluginId } from "../shared/pluginIds.js";
|
||||
|
||||
@@ -46,8 +46,9 @@ interface PiWebPluginServiceOptions {
|
||||
roots?: LocalPluginRoot[];
|
||||
cwd?: string;
|
||||
agentDir?: string;
|
||||
agentDirProvider?: () => string | Promise<string>;
|
||||
packageProvider?: PiPackageProvider | false;
|
||||
configProvider?: () => PiWebConfig;
|
||||
configProvider?: () => PiWebConfig | Promise<PiWebConfig>;
|
||||
}
|
||||
|
||||
interface LocalPluginRoot {
|
||||
@@ -71,11 +72,12 @@ type ArraylessPluginRecord = Omit<PluginRecord, "source" | "scope">;
|
||||
export class DefaultPiPackageProvider implements PiPackageProvider {
|
||||
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({
|
||||
cwd,
|
||||
agentDir,
|
||||
settingsManager: SettingsManager.create(cwd, agentDir),
|
||||
agentDir: resolvedAgentDir,
|
||||
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 {
|
||||
private readonly cwd: string;
|
||||
private readonly roots: LocalPluginRoot[];
|
||||
private readonly packageProvider: PiPackageProvider | undefined;
|
||||
private readonly configProvider: () => PiWebConfig;
|
||||
private readonly agentDir: string | undefined;
|
||||
private readonly agentDirProvider: (() => string | Promise<string>) | undefined;
|
||||
private readonly packageProviderForAgentDir: ((agentDir: string) => PiPackageProvider) | undefined;
|
||||
private readonly configProvider: () => PiWebConfig | Promise<PiWebConfig>;
|
||||
|
||||
constructor(options: PiWebPluginServiceOptions = {}) {
|
||||
const cwd = options.cwd ?? process.cwd();
|
||||
const agentDir = options.agentDir ?? getAgentDir();
|
||||
this.cwd = 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);
|
||||
}
|
||||
|
||||
@@ -110,7 +128,8 @@ export class PiWebPluginService {
|
||||
}
|
||||
|
||||
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)) };
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
for (const plugin of await this.discoverLocalPlugins()) addUnique(records, plugin);
|
||||
if (this.packageProvider !== undefined) {
|
||||
for (const plugin of await this.discoverPiPackagePlugins(this.packageProvider)) addUnique(records, plugin);
|
||||
const packageProvider = await this.packageProvider(config);
|
||||
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));
|
||||
}
|
||||
|
||||
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[]> {
|
||||
const plugins: PluginRecord[] = [];
|
||||
for (const root of this.roots) plugins.push(...await discoverLocalRoot(root));
|
||||
|
||||
@@ -52,7 +52,7 @@ describe("PI WEB status", () => {
|
||||
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" });
|
||||
} finally {
|
||||
@@ -85,12 +85,12 @@ describe("PI WEB status", () => {
|
||||
{ kind: "pi-package", source: "npm:@jmfederico/pi-web", scope: "user", path: "/tmp/pi-web" },
|
||||
"pi-web restart",
|
||||
{
|
||||
agentCommand: "/tmp/agent's/omp",
|
||||
hasCommand: (command) => Promise.resolve(command === "/tmp/agent's/omp"),
|
||||
agentCommand: "/tmp/agent's/alt-agent",
|
||||
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 () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ export interface PiWebStatusCacheOptions {
|
||||
export interface PiWebStatusCache {
|
||||
get(): Promise<PiWebStatusResponse>;
|
||||
refresh(): Promise<PiWebStatusResponse>;
|
||||
invalidate(): void;
|
||||
}
|
||||
|
||||
export function createPiWebStatusCache(load: () => Promise<PiWebStatusResponse>, options: PiWebStatusCacheOptions = {}): PiWebStatusCache {
|
||||
@@ -42,5 +43,8 @@ export function createPiWebStatusCache(load: () => Promise<PiWebStatusResponse>,
|
||||
return refresh();
|
||||
},
|
||||
refresh,
|
||||
invalidate(): void {
|
||||
cached = undefined;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,9 +44,9 @@ describe("AuthService", () => {
|
||||
const agentDir = await tempAgentDir();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -60,12 +60,12 @@ describe("SessionDirResolver", () => {
|
||||
expect(resolver.resolve(cwd)).toMatchObject({ source: "env", sessionDir: envDir, usesConfiguredSessionDir: true });
|
||||
});
|
||||
|
||||
it("uses OMP sessionDir environment overrides before settings", async () => {
|
||||
const envDir = join(tempDir, "omp-env-sessions");
|
||||
it("uses PI WEB sessionDir environment overrides before settings", async () => {
|
||||
const envDir = join(tempDir, "pi-web-env-sessions");
|
||||
await mkdir(agentDir, { recursive: true });
|
||||
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 });
|
||||
});
|
||||
@@ -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 })]));
|
||||
});
|
||||
|
||||
it("includes command-specific env session directories in global listing", async () => {
|
||||
for (const envKey of ["PI_WEB_AGENT_SESSION_DIR", "OMP_CODING_AGENT_SESSION_DIR"]) {
|
||||
it("includes generic env session directories in global listing", async () => {
|
||||
for (const envKey of ["PI_WEB_AGENT_SESSION_DIR", "PI_CODING_AGENT_SESSION_DIR"]) {
|
||||
const envSessionDir = join(tempDir, `${envKey.toLowerCase()}-sessions`);
|
||||
await writeSessionFile(envSessionDir, `${envKey.toLowerCase()}-session`, cwd);
|
||||
const gateway = createPiSessionManagerGateway({
|
||||
agentDir,
|
||||
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");
|
||||
|
||||
@@ -2,12 +2,11 @@ import type { Dirent } from "node:fs";
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
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 type { PiSessionListEntry, PiSessionManager, PiSessionManagerGateway } from "./piSessionService.js";
|
||||
|
||||
export const PI_SESSION_DIR_ENV = "PI_CODING_AGENT_SESSION_DIR";
|
||||
|
||||
type SessionDirSource = "env" | "settings" | "pi-default";
|
||||
|
||||
export interface SessionDirResolution {
|
||||
@@ -28,9 +27,9 @@ export class SessionDirResolver {
|
||||
private readonly sessionDirEnvKeys: readonly string[];
|
||||
|
||||
constructor(options: SessionDirResolverOptions = {}) {
|
||||
this.agentDir = options.agentDir ?? getAgentDir();
|
||||
this.agentDir = options.agentDir ?? effectiveAgentConfig().dir;
|
||||
this.env = options.env ?? process.env;
|
||||
this.sessionDirEnvKeys = options.sessionDirEnvKeys ?? [PI_SESSION_DIR_ENV];
|
||||
this.sessionDirEnvKeys = options.sessionDirEnvKeys ?? agentSessionDirEnvKeys();
|
||||
}
|
||||
|
||||
defaultSessionsRoot(): string {
|
||||
@@ -131,11 +130,11 @@ function uniqueSessionsByPath(sessions: readonly PiSessionListEntry[]): PiSessio
|
||||
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");
|
||||
}
|
||||
|
||||
export function defaultPiSessionDir(cwd: string, agentDir = getAgentDir()): string {
|
||||
export function defaultPiSessionDir(cwd: string, agentDir = effectiveAgentConfig().dir): string {
|
||||
return sessionDirInDefaultPiStore(defaultPiSessionsRoot(agentDir), cwd);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
createAgentSessionServices,
|
||||
createEditToolDefinition,
|
||||
defineTool,
|
||||
getAgentDir,
|
||||
ModelRegistry,
|
||||
SessionManager,
|
||||
type CreateAgentSessionRuntimeFactory,
|
||||
@@ -25,6 +24,7 @@ import { createModelRegistryForAgentDir, type AuthChange } from "./authService.j
|
||||
import { fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
|
||||
import { computeEditPreview, type EditPreviewResult } from "./editPreview.js";
|
||||
import { createPiSessionManagerGateway } from "./piSessionManagerGateway.js";
|
||||
import { effectiveAgentConfig } from "../../config.js";
|
||||
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
|
||||
import { parsePromptAttachments } from "../../shared/promptAttachments.js";
|
||||
import type { SavedPromptAttachment } from "../../shared/apiTypes.js";
|
||||
@@ -338,7 +338,7 @@ export class PiSessionService implements SessionRouteService {
|
||||
|
||||
constructor(private readonly events: SessionEventHub, deps: PiSessionServiceDependencies = {}) {
|
||||
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.modelRegistry = deps.modelRegistry ?? createModelRegistryForAgentDir(this.agentDir);
|
||||
this.spawnTargets = deps.spawnTargets;
|
||||
|
||||
@@ -89,7 +89,7 @@ export interface PiWebConfigValues {
|
||||
* while the capability stabilizes. Requires spawnSessions to be enabled.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user