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>,
<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
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.
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
+4 -4
View File
@@ -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
View File
@@ -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
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 {
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
View File
@@ -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
View File
@@ -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);
+2 -2
View File
@@ -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);
});
+6 -7
View File
@@ -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),
};
}
+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);
});
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");
+46 -14
View File
@@ -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));
+4 -4
View File
@@ -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 () => {
+4
View File
@@ -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;
},
};
}
+2 -2
View File
@@ -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);
}
+2 -2
View File
@@ -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;
+1 -1
View File
@@ -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;
}