From f6e772ac7134a58b019f941d61f86cc2ee99c2a4 Mon Sep 17 00:00:00 2001 From: John Rowe Date: Sat, 26 Sep 2026 10:16:46 +0000 Subject: [PATCH] feat: port to omp 18.3 SDK (settings registry, namespaced AuthStorage, pi-tui moves) omp 18.2 replaced the path-keyed Settings API with typed registry handles, namespaced AuthStorage (credentials/keys/oauth) and moved theme, thinking and goal-tool modules to @oh-my-pi/pi-tui. lib/omp-settings.ts is the single path->handle adapter. Also guards NO_PROXY for loopback: Bun 1.4 proxies loopback requests, which broke local providers behind HTTP_PROXY. Co-Authored-By: Claude Opus 5.5 (1M context) --- .gitignore | 2 +- app/api/agent/new/route.ts | 2 +- app/api/auth/api-key/[provider]/route.ts | 10 ++-- app/api/auth/login/[provider]/route.ts | 2 +- app/api/auth/logout/[provider]/route.ts | 4 +- app/api/display-settings/route.ts | 3 +- app/api/model-roles/route.ts | 5 +- app/api/models/route.ts | 3 +- app/api/settings/route.ts | 17 +++--- bin/omp-web.js | 3 +- bin/runtime.js | 21 +++++++ bun.lock | 54 ++++++++--------- hooks/useDisplaySettings.test.mjs | 2 +- lib/api-types.ts | 2 +- lib/auto-thinking.test.mjs | 3 +- lib/goal-mode.test.mjs | 7 ++- lib/goal-mode.ts | 8 ++- lib/http-dispatcher.test.mjs | 10 +++- lib/model-scope.ts | 2 +- lib/omp-settings.ts | 76 ++++++++++++++++++++++++ lib/omp-theme.ts | 7 ++- lib/omp-types.ts | 3 +- lib/provider-api-key-route.test.mjs | 6 +- lib/provider-listing-runtime.ts | 6 +- lib/rpc-manager.ts | 5 +- lib/runtime.test.mjs | 14 +++++ lib/skills-service.ts | 3 +- lib/startup-preferences.test.mjs | 4 +- lib/startup-preferences.ts | 5 +- package.json | 10 ++-- src-tauri/src/main.rs | 34 +++++++++++ 31 files changed, 250 insertions(+), 83 deletions(-) create mode 100644 lib/omp-settings.ts diff --git a/.gitignore b/.gitignore index a623a02447..b45ae62c94 100644 --- a/.gitignore +++ b/.gitignore @@ -43,4 +43,4 @@ next-env.d.ts src-tauri/target/ src-tauri/gen/ src-tauri/resources/ -src-tauri/server/ \ No newline at end of file +src-tauri/server/.remember/ diff --git a/app/api/agent/new/route.ts b/app/api/agent/new/route.ts index aa7be297cc..60adfb2ddb 100644 --- a/app/api/agent/new/route.ts +++ b/app/api/agent/new/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { parseConfiguredThinkingLevel as parseOmpThinkingLevel, type ConfiguredThinkingLevel } from "@oh-my-pi/pi-coding-agent/thinking"; +import { parseConfiguredThinkingLevel as parseOmpThinkingLevel, type ConfiguredThinkingLevel } from "@oh-my-pi/pi-tui/thinking"; import { existsSync } from "fs"; import { randomUUID } from "crypto"; import { allowFileRoot, getAllowedFileRoots, isExistingFilePathAllowed } from "@/lib/file-access"; diff --git a/app/api/auth/api-key/[provider]/route.ts b/app/api/auth/api-key/[provider]/route.ts index 987d8d32cb..761d8490dc 100644 --- a/app/api/auth/api-key/[provider]/route.ts +++ b/app/api/auth/api-key/[provider]/route.ts @@ -10,12 +10,12 @@ type Params = { params: Promise<{ provider: string }> }; export async function GET(_req: Request, { params }: Params) { const { provider } = await params; const { modelRegistry, authStorage } = await getOmpRuntime(); - const origin = authStorage.getCredentialOrigin(provider); + const origin = authStorage.keys.source(provider); const models = modelRegistry.getAll().filter((model) => model.provider === provider).length; return NextResponse.json({ provider, displayName: provider, - configured: authStorage.hasAuth(provider), + configured: authStorage.keys.source(provider) !== undefined, source: origin?.kind, models, }); @@ -35,7 +35,7 @@ export async function POST(req: Request, { params }: Params) { } // omp stores one row per credential in `agent.db`; writing through // AuthStorage keeps the CLI and omp-web on the same store and lock. - await authStorage.set(provider, { type: "api_key", key: apiKey.trim(), source: "login" }); + await authStorage.credentials.set(provider, { type: "api_key", key: apiKey.trim(), source: "login" }); invalidateModelsCache(); return NextResponse.json({ success: true }); } catch (error) { @@ -48,14 +48,14 @@ export async function DELETE(_req: Request, { params }: Params) { const { provider } = await params; try { const { authStorage } = await getOmpRuntime(); - const stored = authStorage.listStoredCredentials(provider); + const stored = authStorage.credentials.list(provider); if (stored.some((entry) => entry.credential.type === "oauth")) { return NextResponse.json( { error: `${provider} is authenticated with OAuth, not an API key` }, { status: 409 }, ); } - await authStorage.remove(provider); + await authStorage.credentials.remove(provider); invalidateModelsCache(); invalidateOmpRuntime(); return NextResponse.json({ success: true }); diff --git a/app/api/auth/login/[provider]/route.ts b/app/api/auth/login/[provider]/route.ts index 56f4c1d50e..69cdde10b1 100644 --- a/app/api/auth/login/[provider]/route.ts +++ b/app/api/auth/login/[provider]/route.ts @@ -119,7 +119,7 @@ export async function GET( abort.signal.addEventListener("abort", cleanup); try { - await authStorage.login(loginId, { + await authStorage.oauth.login(loginId, { // Every provider prompt (paste-the-code, enterprise URL, ...) becomes // a browser input request keyed by a short-lived token. onPrompt: async (prompt: OAuthPrompt) => { diff --git a/app/api/auth/logout/[provider]/route.ts b/app/api/auth/logout/[provider]/route.ts index b3368b819d..0fbe5ac51c 100644 --- a/app/api/auth/logout/[provider]/route.ts +++ b/app/api/auth/logout/[provider]/route.ts @@ -14,12 +14,12 @@ export async function POST( } const { authStorage } = await getOmpRuntime(); - const stored = authStorage.listStoredCredentials(provider); + const stored = authStorage.credentials.list(provider); if (stored.length > 0 && !stored.some((entry) => entry.credential.type === "oauth")) { return Response.json({ error: `${provider} is authenticated with an API key, not OAuth` }, { status: 409 }); } - await authStorage.logout(provider); + await authStorage.credentials.remove(provider); invalidateModelsCache(); invalidateOmpRuntime(); return Response.json({ ok: true }); diff --git a/app/api/display-settings/route.ts b/app/api/display-settings/route.ts index c73ede22e0..304f030eff 100644 --- a/app/api/display-settings/route.ts +++ b/app/api/display-settings/route.ts @@ -3,6 +3,7 @@ import { getAllowedFileRoots, isExistingFilePathAllowed } from "@/lib/file-acces import { getSettingsForCwd } from "@/lib/omp-runtime"; import { isApiRequestAllowed } from "@/lib/request-security"; import type { DisplaySettings } from "@/lib/settings-api"; +import { getSetting } from "@/lib/omp-settings"; export const dynamic = "force-dynamic"; @@ -25,7 +26,7 @@ export async function GET(req: Request) { } const settings = await getSettingsForCwd(cwd); const payload: DisplaySettings = { - hideThinkingBlock: settings.get("hideThinkingBlock") === true, + hideThinkingBlock: getSetting(settings, "hideThinkingBlock") === true, }; return NextResponse.json(payload); } catch (error) { diff --git a/app/api/model-roles/route.ts b/app/api/model-roles/route.ts index c8edd886f1..4e9d844477 100644 --- a/app/api/model-roles/route.ts +++ b/app/api/model-roles/route.ts @@ -6,6 +6,7 @@ import { getOmpRuntime, getSettingsForCwd } from "@/lib/omp-runtime"; import { listModelRoles, writeModelRole, type ModelRoleScope } from "@/lib/model-roles"; import { resolveVisibleModels } from "@/lib/model-scope"; import { hasJsonContentType, isApiRequestAllowed } from "@/lib/request-security"; +import { getSetting } from "@/lib/omp-settings"; export const dynamic = "force-dynamic"; @@ -27,7 +28,7 @@ export async function GET(req: Request) { try { const { modelRegistry } = await getOmpRuntime(); const settings = await getSettingsForCwd(result.cwd); - const { visible } = await resolveVisibleModels(modelRegistry, settings.get("enabledModels"), settings); + const { visible } = await resolveVisibleModels(modelRegistry, getSetting(settings, "enabledModels"), settings); return NextResponse.json({ roles: listModelRoles(settings, [...visible]) }); } catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 }); @@ -78,7 +79,7 @@ export async function PUT(req: Request) { // the response and in the next modal load. const settings = await getSettingsForCwd(result.cwd); const { modelRegistry } = runtime; - const { visible } = await resolveVisibleModels(modelRegistry, settings.get("enabledModels"), settings); + const { visible } = await resolveVisibleModels(modelRegistry, getSetting(settings, "enabledModels"), settings); return NextResponse.json({ roles: listModelRoles(settings, [...visible]) }); } catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : String(error) }, { status: 500 }); diff --git a/app/api/models/route.ts b/app/api/models/route.ts index 7cdea1bc32..922ac4afe6 100644 --- a/app/api/models/route.ts +++ b/app/api/models/route.ts @@ -11,6 +11,7 @@ import { resolveVisibleModels, selectInitialModelScope } from "@/lib/model-scope import { listModelRoles, readDefaultModelRole } from "@/lib/model-roles"; import { getAllowedFileRoots, isExistingFilePathAllowed } from "@/lib/file-access"; import { getOmpRuntime, getSettingsForCwd } from "@/lib/omp-runtime"; +import { getSetting } from "@/lib/omp-settings"; export const dynamic = "force-dynamic"; @@ -35,7 +36,7 @@ async function loadModels(cwd: string): Promise { const modelError = modelRegistry.getError()?.message; // `enabledModels` supports globs and fuzzy patterns, so resolve it the same // way the CLI does instead of comparing pattern strings literally. - const scope = await resolveVisibleModels(modelRegistry, settings.get("enabledModels"), settings); + const scope = await resolveVisibleModels(modelRegistry, getSetting(settings, "enabledModels"), settings); const { visible, thinkingLevelPins, warnings } = scope; const modelList = visible.map((m) => ({ id: m.id, diff --git a/app/api/settings/route.ts b/app/api/settings/route.ts index 031c4f2f21..9a6a5dbcab 100644 --- a/app/api/settings/route.ts +++ b/app/api/settings/route.ts @@ -7,12 +7,15 @@ import { getUi, hasUi, isCredential, - SETTINGS_SCHEMA, SETTING_TABS, TAB_GROUPS, TAB_METADATA, + findSetting, + getSetting, + isSettingConfigured, + setSetting, type SettingPath, -} from "@oh-my-pi/pi-coding-agent/config/settings-schema"; +} from "@/lib/omp-settings"; import { getOmpRuntime, getSettingsForCwd } from "@/lib/omp-runtime"; import { getAllowedFileRoots, isExistingFilePathAllowed } from "@/lib/file-access"; import { getAvailableWebThemes, getWebThemeConfig } from "@/lib/omp-theme"; @@ -142,9 +145,9 @@ export async function GET(req: Request) { label: ui.label, description: ui.description, type: fieldType, - value: secret ? null : serializableValue(settings.get(path)), + value: secret ? null : serializableValue(getSetting(settings, path)), defaultValue: secret ? null : serializableValue(getDefault(path)), - configured: settings.isConfigured(path), + configured: isSettingConfigured(settings, path), options: optionsFor(path, themeNames), ordered: ui.ordered === true, condition: ui.condition, @@ -168,7 +171,7 @@ export async function GET(req: Request) { export async function PATCH(req: Request) { try { const body = await req.json() as { path?: string; value?: unknown }; - if (!body.path || !(body.path in SETTINGS_SCHEMA) || !hasUi(body.path as SettingPath)) { + if (!body.path || !findSetting(body.path) || !hasUi(body.path as SettingPath)) { return NextResponse.json({ error: "Unknown setting" }, { status: 400 }); } const path = body.path as SettingPath; @@ -180,9 +183,9 @@ export async function PATCH(req: Request) { throw new Error("Unknown omp theme"); } const { settings } = await getOmpRuntime(); - settings.set(path, value as never); + setSetting(settings, path, value); await settings.flush(); - return NextResponse.json({ success: true, value: serializableValue(settings.get(path)) }); + return NextResponse.json({ success: true, value: serializableValue(getSetting(settings, path)) }); } catch (error) { return NextResponse.json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 }); } diff --git a/bin/omp-web.js b/bin/omp-web.js index 24943a211c..b80cbecf61 100644 --- a/bin/omp-web.js +++ b/bin/omp-web.js @@ -8,6 +8,7 @@ const { isBunVersionSupported, isNodeVersionSupported, resolveBunPath, + withLoopbackNoProxy, // eslint-disable-next-line @typescript-eslint/no-require-imports } = require("./runtime"); @@ -158,7 +159,7 @@ function startServer() { cwd: pkgDir, stdio: ["inherit", "pipe", "inherit"], env: { - ...process.env, + ...withLoopbackNoProxy(process.env), OMP_WEB_HOSTNAME: hostname, OMP_WEB_AUTH_FILE: webAuthFile, // Preserve the directory from which `omp-web` was launched so relative diff --git a/bin/runtime.js b/bin/runtime.js index 43067cb992..5ffbc64ab5 100644 --- a/bin/runtime.js +++ b/bin/runtime.js @@ -104,6 +104,26 @@ function getMissingBunMessage() { ].join("\n"); } +const LOOPBACK_HOSTS = ["localhost", "127.0.0.1", "::1"]; + +/** + * Bun 1.4 started sending loopback requests through `HTTP_PROXY` too, which + * breaks local providers (ollama, lm-studio, llama.cpp) behind a proxy. Bun + * honors `NO_PROXY` for them, and reads the proxy environment only once at + * startup, so the launcher has to add loopback before Bun is spawned. + */ +function withLoopbackNoProxy(env) { + const proxied = ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"].some((name) => env[name]); + if (!proxied) return env; + const current = env.NO_PROXY ?? env.no_proxy ?? ""; + if (current.trim() === "*") return env; + const entries = current.split(",").map((entry) => entry.trim()).filter(Boolean); + const missing = LOOPBACK_HOSTS.filter((host) => !entries.includes(host)); + if (missing.length === 0) return env; + const value = [...entries, ...missing].join(","); + return { ...env, NO_PROXY: value, no_proxy: value }; +} + module.exports = { MIN_BUN_VERSION, MIN_NODE_VERSION, @@ -113,4 +133,5 @@ module.exports = { isBunVersionSupported, isNodeVersionSupported, resolveBunPath, + withLoopbackNoProxy, }; diff --git a/bun.lock b/bun.lock index 530a004f67..622cfe4c28 100644 --- a/bun.lock +++ b/bun.lock @@ -5,11 +5,11 @@ "": { "name": "omp-web", "dependencies": { - "@oh-my-pi/pi-agent-core": "~18.1.17", - "@oh-my-pi/pi-ai": "~18.1.17", - "@oh-my-pi/pi-coding-agent": "~18.1.17", - "@oh-my-pi/pi-tui": "~18.1.17", - "@oh-my-pi/pi-utils": "~18.1.17", + "@oh-my-pi/pi-agent-core": "~18.3.2", + "@oh-my-pi/pi-ai": "~18.3.2", + "@oh-my-pi/pi-coding-agent": "~18.3.2", + "@oh-my-pi/pi-tui": "~18.3.2", + "@oh-my-pi/pi-utils": "~18.3.2", "js-yaml": "^5.4.1", "next": "16.2.12", "proper-lockfile": "4.1.2", @@ -316,41 +316,41 @@ "@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, "sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA=="], - "@oh-my-pi/omp-stats": ["@oh-my-pi/omp-stats@18.1.17", "", { "dependencies": { "@oh-my-pi/pi-ai": "18.1.17", "@oh-my-pi/pi-catalog": "18.1.17", "@oh-my-pi/pi-utils": "18.1.17", "@tailwindcss/node": "^4.3.2", "chart.js": "^4.5.1", "lucide-react": "^1.24.0", "react": "19.2.7", "react-chartjs-2": "^5.3.1", "react-dom": "19.2.7", "tailwindcss": "^4.3.2" }, "bin": { "omp-stats": "src/index.ts" } }, "sha512-pQUGaP9012Opxsx/8Utz6OUYtvg6mex+noszhIbr+9wquLrxqtk9G/aFq2UyzzFoquTsFKdRF2hslbqVoFzRYg=="], + "@oh-my-pi/omp-stats": ["@oh-my-pi/omp-stats@18.3.2", "", { "dependencies": { "@oh-my-pi/pi-ai": "18.3.2", "@oh-my-pi/pi-catalog": "18.3.2", "@oh-my-pi/pi-utils": "18.3.2", "@tailwindcss/node": "^4.3.2", "chart.js": "^4.5.1", "lucide-react": "^1.24.0", "react": "19.2.7", "react-chartjs-2": "^5.3.1", "react-dom": "19.2.7", "tailwindcss": "^4.3.2" }, "bin": { "omp-stats": "src/index.ts" } }, "sha512-GjJCrcSrWCv7VLsuYk+2VGNXlRCq42BFxiXsds3iYBwbWvKxrvyOrA9key8iG7ES6/cJPWn6dSfs+tDHDviHAQ=="], - "@oh-my-pi/omptype": ["@oh-my-pi/omptype@18.1.17", "", {}, "sha512-ps3P2vaUTSIk9Pk2xryeTqz4YtEQ/bQlCBQBheiVDkeKO8qco9rCsWu+NRNHqbXnl5WrjlZyk8nEWf+z4+NJIA=="], + "@oh-my-pi/omptype": ["@oh-my-pi/omptype@18.3.2", "", {}, "sha512-x/vzutjo/cw3u/JU8uyuyE4XSbVUQIENwej3wc8iJRRYc6G0HFnvVdGrC/tfYBOSDR2hOGkVoIqKq5cPOCfVbg=="], - "@oh-my-pi/pi-agent-core": ["@oh-my-pi/pi-agent-core@18.1.17", "", { "dependencies": { "@oh-my-pi/pi-ai": "18.1.17", "@oh-my-pi/pi-catalog": "18.1.17", "@oh-my-pi/pi-natives": "18.1.17", "@oh-my-pi/pi-utils": "18.1.17", "@oh-my-pi/pi-wire": "18.1.17", "@oh-my-pi/snapcompact": "18.1.17", "@opentelemetry/api": "^1.9.1" } }, "sha512-bXPzmMPIQYCQ8SWWq7gOdUxflYMMGzJWSb3VbOVpyOHvMPMFlFFxLTSKVNtzHHjUX/xnzwTO0Z+UknfMhnR1Sw=="], + "@oh-my-pi/pi-agent-core": ["@oh-my-pi/pi-agent-core@18.3.2", "", { "dependencies": { "@oh-my-pi/pi-ai": "18.3.2", "@oh-my-pi/pi-catalog": "18.3.2", "@oh-my-pi/pi-natives": "18.3.2", "@oh-my-pi/pi-utils": "18.3.2", "@oh-my-pi/pi-wire": "18.3.2", "@oh-my-pi/snapcompact": "18.3.2", "@opentelemetry/api": "^1.9.1" } }, "sha512-GJYtScoRq+a/EfMBO8k2jfD9kLBv3cQblKn5A8ctFDfdJB1mek2eG1kPrvJshr0kKsduOuOtP6ccGYVPFzPW+A=="], - "@oh-my-pi/pi-ai": ["@oh-my-pi/pi-ai@18.1.17", "", { "dependencies": { "@oh-my-pi/omptype": "18.1.17", "@oh-my-pi/pi-catalog": "18.1.17", "@oh-my-pi/pi-natives": "18.1.17", "@oh-my-pi/pi-utils": "18.1.17", "@oh-my-pi/pi-wire": "18.1.17" } }, "sha512-gQ1ucUcxjq1kyLrin1/TztRXPKbqcP+y+RwbnwURZSCcKP7wa+KKPBQM2w4+5NrK9uozOOFiRgHd1KwwGkOxPA=="], + "@oh-my-pi/pi-ai": ["@oh-my-pi/pi-ai@18.3.2", "", { "dependencies": { "@oh-my-pi/omptype": "18.3.2", "@oh-my-pi/pi-catalog": "18.3.2", "@oh-my-pi/pi-natives": "18.3.2", "@oh-my-pi/pi-utils": "18.3.2", "@oh-my-pi/pi-wire": "18.3.2" } }, "sha512-2VgxiCcNFgt5HJVYSDePYgWm72lIPPSWXD+eAj4rQwd83uZ8NwJJcgG/KkGr0Vwe0RqvfbxK2uDg7Gb8f/UruQ=="], - "@oh-my-pi/pi-catalog": ["@oh-my-pi/pi-catalog@18.1.17", "", { "dependencies": { "@oh-my-pi/omptype": "18.1.17", "@oh-my-pi/pi-utils": "18.1.17" } }, "sha512-eojN8NXoh6jwJPOiB4/uDIs/uSh5WXudVGyx1KELboghJA6qejAuhT8Fbo0DKOx5/iyNa0OXH2fXEf5mwYBp2g=="], + "@oh-my-pi/pi-catalog": ["@oh-my-pi/pi-catalog@18.3.2", "", { "dependencies": { "@oh-my-pi/omptype": "18.3.2", "@oh-my-pi/pi-utils": "18.3.2" } }, "sha512-W+kaE56Wyn0yiL2pAc2LrojC2pmEBJTIqdwLfQSZty7dPMdO9sJ7nhJqu50Mjo3SdkCOILwV7bERWt3eu+xncQ=="], - "@oh-my-pi/pi-coding-agent": ["@oh-my-pi/pi-coding-agent@18.1.17", "", { "dependencies": { "@babel/parser": "^7.29.7", "@oh-my-pi/omp-stats": "18.1.17", "@oh-my-pi/omptype": "18.1.17", "@oh-my-pi/pi-agent-core": "18.1.17", "@oh-my-pi/pi-ai": "18.1.17", "@oh-my-pi/pi-catalog": "18.1.17", "@oh-my-pi/pi-mnemopi": "18.1.17", "@oh-my-pi/pi-natives": "18.1.17", "@oh-my-pi/pi-tui": "18.1.17", "@oh-my-pi/pi-utils": "18.1.17", "@oh-my-pi/pi-wire": "18.1.17", "@oh-my-pi/snapcompact": "18.1.17", "@opentelemetry/api": "^1.9.1", "@opentelemetry/api-logs": "^0.220.0", "@opentelemetry/context-async-hooks": "^2.9.0", "@opentelemetry/exporter-logs-otlp-proto": "^0.220.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.220.0", "@opentelemetry/exporter-trace-otlp-proto": "^0.220.0", "@opentelemetry/resources": "^2.9.0", "@opentelemetry/sdk-logs": "^0.220.0", "@opentelemetry/sdk-metrics": "^2.9.0", "@opentelemetry/sdk-trace-base": "^2.9.0", "@opentelemetry/sdk-trace-node": "^2.9.0", "puppeteer-core": "25.3.0" }, "optionalDependencies": { "@huggingface/transformers": "^4.2.0", "sherpa-onnx-node": "1.13.2" }, "bin": { "omp": "dist/cli.js" } }, "sha512-kkKsQUAStT8O6z9vlDQMwQzMBzUBSxA5vBrGpONEdOrgSVzYMtVWxxm1t6uSVFpN9cophbTCFRV0wRAonc8XVw=="], + "@oh-my-pi/pi-coding-agent": ["@oh-my-pi/pi-coding-agent@18.3.2", "", { "dependencies": { "@babel/parser": "^7.29.7", "@oh-my-pi/omp-stats": "18.3.2", "@oh-my-pi/omptype": "18.3.2", "@oh-my-pi/pi-agent-core": "18.3.2", "@oh-my-pi/pi-ai": "18.3.2", "@oh-my-pi/pi-catalog": "18.3.2", "@oh-my-pi/pi-mnemopi": "18.3.2", "@oh-my-pi/pi-natives": "18.3.2", "@oh-my-pi/pi-tui": "18.3.2", "@oh-my-pi/pi-utils": "18.3.2", "@oh-my-pi/pi-wire": "18.3.2", "@oh-my-pi/snapcompact": "18.3.2", "@opentelemetry/api": "^1.9.1", "@opentelemetry/api-logs": "^0.220.0", "@opentelemetry/context-async-hooks": "^2.9.0", "@opentelemetry/exporter-logs-otlp-proto": "^0.220.0", "@opentelemetry/exporter-metrics-otlp-proto": "^0.220.0", "@opentelemetry/exporter-trace-otlp-proto": "^0.220.0", "@opentelemetry/resources": "^2.9.0", "@opentelemetry/sdk-logs": "^0.220.0", "@opentelemetry/sdk-metrics": "^2.9.0", "@opentelemetry/sdk-trace-base": "^2.9.0", "@opentelemetry/sdk-trace-node": "^2.9.0", "axe-core": "4.13.0", "puppeteer-core": "25.3.0" }, "optionalDependencies": { "@huggingface/transformers": "^4.2.0", "sherpa-onnx-node": "1.13.2" }, "bin": { "omp": "dist/cli.js" } }, "sha512-cCJecQ6Ty48LNhvlDR01qNHxcdf+ZkuY9j1rWUTwN0TrgyO+7a1jFN73B3rsUPw8Kx+SDmRtVzJkHEp6k/QTaw=="], - "@oh-my-pi/pi-mnemopi": ["@oh-my-pi/pi-mnemopi@18.1.17", "", { "dependencies": { "@oh-my-pi/pi-ai": "18.1.17", "@oh-my-pi/pi-catalog": "18.1.17", "@oh-my-pi/pi-natives": "18.1.17", "@oh-my-pi/pi-utils": "18.1.17" }, "peerDependencies": { "fastembed": "2.1.0", "onnxruntime-node": "1.21.0" }, "optionalPeers": ["fastembed", "onnxruntime-node"], "bin": { "mnemopi": "src/cli.ts" } }, "sha512-V6yi0ovHUwiUBO9pqOA8GsoUCr1xkY7n7fafsKYvZ+1bcwgtQBgy9gKUAo+MSyIQmYGjUBoFPH9btdUaZbftjQ=="], + "@oh-my-pi/pi-mnemopi": ["@oh-my-pi/pi-mnemopi@18.3.2", "", { "dependencies": { "@oh-my-pi/pi-ai": "18.3.2", "@oh-my-pi/pi-catalog": "18.3.2", "@oh-my-pi/pi-natives": "18.3.2", "@oh-my-pi/pi-utils": "18.3.2" }, "peerDependencies": { "fastembed": "2.1.0", "onnxruntime-node": "1.21.0" }, "optionalPeers": ["fastembed", "onnxruntime-node"], "bin": { "mnemopi": "src/cli.ts" } }, "sha512-JCMANedPbl++tpan/NR5vHGLdhy1DX1COvbTH2gTZfDFolKcF/FdYzbOOoosJuD8Xk4RknB01oKFuGd5U2NZng=="], - "@oh-my-pi/pi-natives": ["@oh-my-pi/pi-natives@18.1.17", "", { "optionalDependencies": { "@oh-my-pi/pi-natives-darwin-arm64": "18.1.17", "@oh-my-pi/pi-natives-darwin-x64": "18.1.17", "@oh-my-pi/pi-natives-linux-arm64": "18.1.17", "@oh-my-pi/pi-natives-linux-x64": "18.1.17", "@oh-my-pi/pi-natives-win32-arm64": "18.1.17", "@oh-my-pi/pi-natives-win32-x64": "18.1.17" } }, "sha512-kDk6aOdLAaVLheTTBY6DLJqpQu7s8oT38DOH0ZAQyvSfOyir+3vsqSGFY6plCaLBnlUjGGkcwu+XdmBT4SjlIQ=="], + "@oh-my-pi/pi-natives": ["@oh-my-pi/pi-natives@18.3.2", "", { "optionalDependencies": { "@oh-my-pi/pi-natives-darwin-arm64": "18.3.2", "@oh-my-pi/pi-natives-darwin-x64": "18.3.2", "@oh-my-pi/pi-natives-linux-arm64": "18.3.2", "@oh-my-pi/pi-natives-linux-x64": "18.3.2", "@oh-my-pi/pi-natives-win32-arm64": "18.3.2", "@oh-my-pi/pi-natives-win32-x64": "18.3.2" } }, "sha512-CepYRiCR3G0dKP8k0/ufqsoj/wU0tkEEkvEON9q0dgHIGU98AFhWjsxZnjVtnlC3jSb8/2XgC3DD63hkkh53tA=="], - "@oh-my-pi/pi-natives-darwin-arm64": ["@oh-my-pi/pi-natives-darwin-arm64@18.1.17", "", { "os": "darwin", "cpu": "arm64" }, "sha512-3UDyseKTuLAtjr49cfAQX3Hm/Hkn+GeS+KDYCngi+1qy8/Tbrd6HeBXT/5Gfsn4hFmg7VNxlMZ6MIR2eo1ZU+A=="], + "@oh-my-pi/pi-natives-darwin-arm64": ["@oh-my-pi/pi-natives-darwin-arm64@18.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8F9t5+NZlHw14Z3vQySJJQTbWyxMUjGwIeDoogbRvRFMOyo+WQNqpo0scKZh0tNe6nbIz5+IIUVkUiwOXxKSKw=="], - "@oh-my-pi/pi-natives-darwin-x64": ["@oh-my-pi/pi-natives-darwin-x64@18.1.17", "", { "os": "darwin", "cpu": "x64" }, "sha512-VVNcyPLalraBE4E4doeW97A48Z2aQQ+SymattJCr33nXFsJYQx9DEuO2DPy536G36LplLrFNyFoF/9O5IMxk9g=="], + "@oh-my-pi/pi-natives-darwin-x64": ["@oh-my-pi/pi-natives-darwin-x64@18.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-zcR+GopMVByfSh6jUllMh31HHS0RY2116kUS/yNFUpDEeIdm5fduVBkNolQe7mqsxk/o+SqlEFduFaFUYH1IPQ=="], - "@oh-my-pi/pi-natives-linux-arm64": ["@oh-my-pi/pi-natives-linux-arm64@18.1.17", "", { "os": "linux", "cpu": "arm64" }, "sha512-T3TOyAJTK9xZY+aGIzpDzovVPNhNynPrwiBNKn2vZWIpdAeifNiYrubHUFq9abq3RzSEsT3C8MOHKy1EylTr/Q=="], + "@oh-my-pi/pi-natives-linux-arm64": ["@oh-my-pi/pi-natives-linux-arm64@18.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-JohwfrqhSLu13gnVyUXUlgCnsetL5wtXDdMBHrcgpynvP+9xa+bszHaVvVlifU26j3da6QIGUwTbe/jK52I+Yw=="], - "@oh-my-pi/pi-natives-linux-x64": ["@oh-my-pi/pi-natives-linux-x64@18.1.17", "", { "os": "linux", "cpu": "x64" }, "sha512-HXkCqfjIiRaR9yp+eyY92iifA63xXpigUFYSe1/6zsGVeOUrdPBXaouI3tNjE7yRZSC/gjdteOFYGyEefHJWpg=="], + "@oh-my-pi/pi-natives-linux-x64": ["@oh-my-pi/pi-natives-linux-x64@18.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-bS7BeDKwhV7W3lnKv3ZuhzAjZJjc9lR6muTLC8BWDGVzd2LCiQuwyJchfxs0qOXqkZFMinNmBu2qciLwhxfWJg=="], - "@oh-my-pi/pi-natives-win32-arm64": ["@oh-my-pi/pi-natives-win32-arm64@18.1.17", "", { "os": "win32", "cpu": "arm64" }, "sha512-5P9RRwPKMHf4jrKYdZGYFG8U4q3nKd/2y3bMGwbgmOoeGEc1LnFPRKTWWqlnwh71E0VGYD6cCoPSESiizZjbbQ=="], + "@oh-my-pi/pi-natives-win32-arm64": ["@oh-my-pi/pi-natives-win32-arm64@18.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-MBidlin1T1fmomN33ZsjIDv9fs7KbP8JdmqzO342VwU2IMpPeew3QlvQ2XNzllzT0fCOYC7baBrikBpR2FoY+w=="], - "@oh-my-pi/pi-natives-win32-x64": ["@oh-my-pi/pi-natives-win32-x64@18.1.17", "", { "os": "win32", "cpu": "x64" }, "sha512-u5EvPHrkeiTKvTK5XAo9SVbf/WJT7Hgcimu0Te5s+Fe4VW/yFUPehLgU/NOa6jn6EeOfFMFR1ZPeb5bLTG787g=="], + "@oh-my-pi/pi-natives-win32-x64": ["@oh-my-pi/pi-natives-win32-x64@18.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-eSTIyI7SmTu9YFa8D3QUO9FuTMR4KOGqg3fCjANkbbTkbCO8Le9bbmJAqyMJNfYvgvp8WI83davZ4BVv5Zcspg=="], - "@oh-my-pi/pi-tui": ["@oh-my-pi/pi-tui@18.1.17", "", { "dependencies": { "@oh-my-pi/pi-natives": "18.1.17", "@oh-my-pi/pi-utils": "18.1.17" } }, "sha512-Y1vSEMZrabXWqy9tL9CKA1LRYiE8DKmdGFige70c8iF7vmH4fZ0A6R32NaAWIL8bSY3QqmZ5QR90MjPtfEsd1g=="], + "@oh-my-pi/pi-tui": ["@oh-my-pi/pi-tui@18.3.2", "", { "dependencies": { "@oh-my-pi/omptype": "18.3.2", "@oh-my-pi/pi-agent-core": "18.3.2", "@oh-my-pi/pi-ai": "18.3.2", "@oh-my-pi/pi-catalog": "18.3.2", "@oh-my-pi/pi-natives": "18.3.2", "@oh-my-pi/pi-utils": "18.3.2", "@oh-my-pi/pi-wire": "18.3.2", "@oh-my-pi/snapcompact": "18.3.2" } }, "sha512-bV5dBXfmBS9OFW8Vw0rfDlJ9NuJ+tWOsPQaFrls1miSUmebkiuW7HksOBKU11fNsSTNvukMqsqdh6kFMBnnQaw=="], - "@oh-my-pi/pi-utils": ["@oh-my-pi/pi-utils@18.1.17", "", { "dependencies": { "@oh-my-pi/pi-natives": "18.1.17" } }, "sha512-STu5ZAmsoOXQnlMbSOOj/pm9pt21okEmPi5uLXeGZBhMcWUfg9ePke8lNTmJTPBdgAyq5FFitQc+yHPuOMs01A=="], + "@oh-my-pi/pi-utils": ["@oh-my-pi/pi-utils@18.3.2", "", { "dependencies": { "@oh-my-pi/pi-natives": "18.3.2" } }, "sha512-wY3cD7VuqulIUkIE9VLd/7ybhzrdRM5xx5wqziJUfZMWm1cEFscuGa9Z/ODAqi4anVAZyj+V7UKkVthoKl5Izg=="], - "@oh-my-pi/pi-wire": ["@oh-my-pi/pi-wire@18.1.17", "", {}, "sha512-qtVvN8aHGZFmT/bdaaEd8OHH/0R3ieeMeVGMUpUoBA2s706WSffwPJRmpgMqj5yMc6TyEKnKv8TDpAaKnOZkUg=="], + "@oh-my-pi/pi-wire": ["@oh-my-pi/pi-wire@18.3.2", "", {}, "sha512-X/PvcYhJGe9KomSK+x2MMEw82M5Y4ajgj6/fhMZ27ZZxP97Q090321ps42KjgYfwDRuQOMNdxht5KN1mhkXKPQ=="], - "@oh-my-pi/snapcompact": ["@oh-my-pi/snapcompact@18.1.17", "", { "dependencies": { "@oh-my-pi/pi-ai": "18.1.17", "@oh-my-pi/pi-catalog": "18.1.17", "@oh-my-pi/pi-natives": "18.1.17", "@oh-my-pi/pi-utils": "18.1.17", "@oh-my-pi/pi-wire": "18.1.17" } }, "sha512-S2myDQrhOlN9ZbnYK0T+vJc5gj1GW4sdeK9uGktt0XC9vvZIwXJRIwql4S0gIZEggJr7PJ63xuiNhH3VKkWVSA=="], + "@oh-my-pi/snapcompact": ["@oh-my-pi/snapcompact@18.3.2", "", { "dependencies": { "@oh-my-pi/pi-ai": "18.3.2", "@oh-my-pi/pi-catalog": "18.3.2", "@oh-my-pi/pi-natives": "18.3.2", "@oh-my-pi/pi-utils": "18.3.2", "@oh-my-pi/pi-wire": "18.3.2" } }, "sha512-bGlQEFRw74/10gEs99RMyEggtxF29STdcIlxwpCAKlAi4sNs8tZh/j1YRelzmVQI3Bsd2SQzlg27O8O2zuThkg=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], @@ -856,7 +856,7 @@ "available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], - "axe-core": ["axe-core@4.12.1", "", {}, "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA=="], + "axe-core": ["axe-core@4.13.0", "", {}, "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], @@ -1030,8 +1030,6 @@ "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], - "date-fns": ["date-fns@4.4.0", "", {}, "sha512-+1UMbeh68lH1SegH83CGWwpb6OHHbpSgr3+s5Eww5M4CAgswBpoWS0AjTOfEJ33HiYKz1hdj/KTFprzXHmq/6w=="], - "dayjs": ["dayjs@1.11.21", "", {}, "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], @@ -1662,8 +1660,6 @@ "modern-tar": ["modern-tar@0.7.7", "", {}, "sha512-t9VmxaqrmANnEOBhpSDI6HD192Ge48k8vmWqQQL7hSFEqHEYwZbbsu49+aKLWZeRvFs3j1pMhXOqqF4kPlvjkQ=="], - "moment": ["moment@2.30.1", "", {}, "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how=="], - "motion": ["motion@12.43.0", "", { "dependencies": { "framer-motion": "^12.43.0", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-BQgQbSa9Hn3/mtbib0MK53y6JSANa+YKUKlaYnWzAVDH424RYQ5LVpV3pNiWH00BA2z4ojsSdMzqT7g2FQwjuQ=="], "motion-dom": ["motion-dom@12.43.0", "", { "dependencies": { "motion-utils": "^12.39.0" } }, "sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag=="], @@ -2306,6 +2302,8 @@ "eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "eslint-plugin-jsx-a11y/axe-core": ["axe-core@4.12.1", "", {}, "sha512-s7iGf5GaVMxEG0ENN9x+xTr7GFZCb1ZP/1uATUpCEK2X78nDB3RwbtFCo9pGAf9ru+VwoQ464DkaLEeRM08wJA=="], + "extend-shallow/is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], diff --git a/hooks/useDisplaySettings.test.mjs b/hooks/useDisplaySettings.test.mjs index ad6f39ef87..9cd514ffc8 100644 --- a/hooks/useDisplaySettings.test.mjs +++ b/hooks/useDisplaySettings.test.mjs @@ -8,7 +8,7 @@ const settingsSource = readFileSync(new URL("../components/SettingsConfig.tsx", test("serves the render-affecting settings for the requested cwd", () => { assert.match(routeSource, /getSettingsForCwd\(cwd\)/); - assert.match(routeSource, /hideThinkingBlock: settings\.get\("hideThinkingBlock"\) === true/); + assert.match(routeSource, /hideThinkingBlock: getSetting\(settings, "hideThinkingBlock"\) === true/); // Same guards as the other settings-derived endpoints. assert.match(routeSource, /isApiRequestAllowed\(req\)/); assert.match(routeSource, /isExistingFilePathAllowed\(cwd, allowedRoots\)/); diff --git a/lib/api-types.ts b/lib/api-types.ts index b39faafe67..c4e7fba217 100644 --- a/lib/api-types.ts +++ b/lib/api-types.ts @@ -146,7 +146,7 @@ export interface ModelRoleAssignment { /** Which persisted layer supplies the effective value. */ source: ModelRoleScope | "default"; /** Full merge provenance, including runtime and config-overlay layers. */ - provenance: "runtime" | "overlay" | "project" | "global" | "default"; + provenance: "env" | "runtime" | "overlay" | "project" | "global" | "default"; /** The model the selector currently resolves to, when it resolves at all. */ resolved?: ModelRoleModelRef; /** Resolver complaint, e.g. a selector that matches nothing available. */ diff --git a/lib/auto-thinking.test.mjs b/lib/auto-thinking.test.mjs index e1fa7584f7..26c69d129a 100644 --- a/lib/auto-thinking.test.mjs +++ b/lib/auto-thinking.test.mjs @@ -1,6 +1,7 @@ import assert from "node:assert/strict"; import test from "node:test"; import { Settings } from "@oh-my-pi/pi-coding-agent"; +import { getSetting } from "./omp-settings.ts"; import { buildSessionContext } from "./session-reader.ts"; import { persistExplicitStartupPreferences } from "./startup-preferences.ts"; @@ -31,5 +32,5 @@ test("explicit Auto persists as Auto rather than the first resolved effort", asy { thinkingLevel: "auto" }, { thinkingLevel: "high", supportsThinking: true }, ); - assert.equal(settings.get("defaultThinkingLevel"), "auto"); + assert.equal(getSetting(settings, "defaultThinkingLevel"), "auto"); }); diff --git a/lib/goal-mode.test.mjs b/lib/goal-mode.test.mjs index 9d94d4125a..c493aec6d6 100644 --- a/lib/goal-mode.test.mjs +++ b/lib/goal-mode.test.mjs @@ -1,5 +1,6 @@ import assert from "node:assert/strict"; import test from "node:test"; +import { Settings } from "@oh-my-pi/pi-coding-agent"; async function loadSubject() { return import("./goal-mode.ts"); @@ -21,11 +22,11 @@ function makeGoal(overrides = {}) { /** A stand-in for the SDK session, driving the same GoalRuntime call shapes. */ function makeSession(options = {}) { const entries = options.entries ?? []; - const settings = new Map(Object.entries({ + const settings = Settings.isolated({ "goal.enabled": true, "goal.continuationModes": ["interactive"], ...options.settings, - })); + }); let state = options.state; const calls = { tools: [], modeChanges: [], customEntries: [], goalContext: 0 }; let tools = options.tools ?? ["read", "bash"]; @@ -34,7 +35,7 @@ function makeSession(options = {}) { isStreaming: options.isStreaming ?? false, calls, get toolNames() { return tools; }, - settings: { get: (path) => settings.get(path) }, + settings, sessionManager: { getEntries: () => entries, appendModeChange: (mode, data) => calls.modeChanges.push({ mode, data }), diff --git a/lib/goal-mode.ts b/lib/goal-mode.ts index 2063632959..c193cbfda3 100644 --- a/lib/goal-mode.ts +++ b/lib/goal-mode.ts @@ -1,5 +1,7 @@ -import type { Goal, GoalModeState } from "@oh-my-pi/pi-coding-agent/goals/state"; +import type { GoalModeState } from "@oh-my-pi/pi-coding-agent/goals/state"; +import type { Goal } from "@oh-my-pi/pi-tui/tools/goal"; import type { GoalModeSession, GoalStatusInfo } from "./omp-types"; +import { getSetting } from "@/lib/omp-settings"; export type { GoalStatusInfo } from "./omp-types"; @@ -438,7 +440,7 @@ export class GoalModeController { #scheduleContinuation(): void { this.#cancelContinuation(); if (this.#disposed) return; - if (!isGoalContinuationEnabled(this.#session.settings.get("goal.continuationModes"))) return; + if (!isGoalContinuationEnabled(getSetting(this.#session.settings, "goal.continuationModes"))) return; if (this.#session.getPlanModeState?.()?.enabled) return; if (this.#suppressNextContinuation) return; const state = this.#session.getGoalModeState?.(); @@ -486,7 +488,7 @@ export class GoalModeController { } #goalSettingEnabled(): boolean { - return this.#session.settings.get("goal.enabled") !== false; + return getSetting(this.#session.settings, "goal.enabled") !== false; } #pausedGoal(): GoalModeState | undefined { diff --git a/lib/http-dispatcher.test.mjs b/lib/http-dispatcher.test.mjs index ccaf1a27db..bdf2ba9af9 100644 --- a/lib/http-dispatcher.test.mjs +++ b/lib/http-dispatcher.test.mjs @@ -3,9 +3,11 @@ import { spawn } from "node:child_process"; import { createServer } from "node:http"; import { once } from "node:events"; import test from "node:test"; +import { createRequire } from "node:module"; import { createJiti } from "jiti"; const jiti = createJiti(import.meta.url); +const { withLoopbackNoProxy } = createRequire(import.meta.url)("../bin/runtime.js"); const { configureHttpDispatcher, isBunRuntime } = await jiti.import("./http-dispatcher.ts"); /** @@ -84,7 +86,9 @@ bunOnly("Bun's fetch routes remote hosts through HTTP_PROXY and reaches loopback `; const child = spawn(process.execPath, ["-e", script], { - env: { ...process.env, HTTP_PROXY: proxy.url, HTTPS_PROXY: proxy.url }, + // The launcher (bin/runtime.js) adds loopback to NO_PROXY: Bun 1.4+ would + // otherwise proxy loopback too. + env: withLoopbackNoProxy({ ...process.env, HTTP_PROXY: proxy.url, HTTPS_PROXY: proxy.url }), stdio: ["ignore", "pipe", "inherit"], }); let output = ""; @@ -94,7 +98,9 @@ bunOnly("Bun's fetch routes remote hosts through HTTP_PROXY and reaches loopback assert.equal(code, 0, output); assert.match(output, /proxied:204/); // Bun forwards plain HTTP as an absolute-URI request rather than CONNECT. - assert.deepEqual(proxy.forwardedRequests, ["GET http://target.invalid:80/through-http-proxy"]); + // Bun 1.4 dropped the default port from the forwarded URI. + assert.equal(proxy.forwardedRequests.length, 1); + assert.match(proxy.forwardedRequests[0], /^GET http:\/\/target\.invalid(?::80)?\/through-http-proxy$/); // Local providers (ollama, lm-studio, llama.cpp) must not be proxied. assert.match(output, /loopback:direct/); assert.equal(proxy.forwardedRequests.length + proxy.connectTargets.length, 1); diff --git a/lib/model-scope.ts b/lib/model-scope.ts index 50d431c52a..aae338af71 100644 --- a/lib/model-scope.ts +++ b/lib/model-scope.ts @@ -1,4 +1,4 @@ -import type { ConfiguredThinkingLevel } from "@oh-my-pi/pi-coding-agent/thinking"; +import type { ConfiguredThinkingLevel } from "@oh-my-pi/pi-tui/thinking"; import type { ModelRegistry, Settings } from "@oh-my-pi/pi-coding-agent"; import { resolveModelScope, type ScopedModel } from "@oh-my-pi/pi-coding-agent/config/model-resolver"; import type { Api, Model } from "@oh-my-pi/pi-ai"; diff --git a/lib/omp-settings.ts b/lib/omp-settings.ts new file mode 100644 index 0000000000..ca8452c6ea --- /dev/null +++ b/lib/omp-settings.ts @@ -0,0 +1,76 @@ +// omp 18.2 replaced the path-keyed `settings.get("a.b")` / `settings-schema` +// surface with a registry of typed `Setting` handles (`config/registry`). +// omp-web addresses settings by their dotted path — the browser sends paths, +// and the settings panel is generated from the schema — so this module is the +// single place that turns a path into a handle. Importing `all-settings` +// registers every domain, exactly as omp's own settings panel does. +import { orderedSettings } from "@oh-my-pi/pi-coding-agent/config/all-settings"; +import { type AnySetting, lookup } from "@oh-my-pi/pi-coding-agent/config/registry"; +import { cfgSkills, type SkillsSettings } from "@oh-my-pi/pi-coding-agent/extensibility/settings"; +import type { Settings } from "@oh-my-pi/pi-coding-agent"; +import type { AnyUiMetadata, SettingTab } from "@oh-my-pi/pi-tui/overlays/settings-defs"; + +export { SETTING_TABS, TAB_GROUPS, TAB_METADATA, type SettingTab } from "@oh-my-pi/pi-tui/overlays/settings-defs"; + +export type SettingPath = string; + +// Registration happens on import; touching the ordered list keeps a bundler +// from dropping the side-effect-only import. +orderedSettings(); + +export function findSetting(path: SettingPath): AnySetting | undefined { + return lookup(path); +} + +function requireSetting(path: SettingPath): AnySetting { + const setting = lookup(path); + if (!setting) throw new Error(`Unknown setting: ${path}`); + return setting; +} + +/** Effective value (environment included), `undefined` for an unknown path. */ +export function getSetting(settings: Settings, path: SettingPath): T | undefined { + return lookup(path)?.get(settings) as T | undefined; +} + +/** Persists `value` to the global config layer. */ +export function setSetting(settings: Settings, path: SettingPath, value: unknown): void { + requireSetting(path).set(settings, value); +} + +export function isSettingConfigured(settings: Settings, path: SettingPath): boolean { + return lookup(path)?.isConfigured(settings) ?? false; +} + +export function getSkillsSettings(settings: Settings): SkillsSettings { + return cfgSkills.get(settings); +} + +export function getDefault(path: SettingPath): unknown { + return lookup(path)?.default; +} + +export function getType(path: SettingPath): string | undefined { + return lookup(path)?.type; +} + +export function getEnumValues(path: SettingPath): readonly string[] | undefined { + return lookup(path)?.enumValues; +} + +export function getUi(path: SettingPath): AnyUiMetadata | undefined { + return lookup(path)?.ui; +} + +export function hasUi(path: SettingPath): boolean { + return lookup(path)?.ui !== undefined; +} + +export function isCredential(path: SettingPath): boolean { + return lookup(path)?.isCredential ?? false; +} + +/** Paths shown on `tab`, in omp's settings-panel order. */ +export function getPathsForTab(tab: SettingTab): SettingPath[] { + return orderedSettings().filter((setting) => setting.ui?.tab === tab).map((setting) => setting.id); +} diff --git a/lib/omp-theme.ts b/lib/omp-theme.ts index e4d9fab549..028048d560 100644 --- a/lib/omp-theme.ts +++ b/lib/omp-theme.ts @@ -3,9 +3,10 @@ import { getResolvedThemeColors, getThemeExportColors, isLightTheme, -} from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; +} from "@oh-my-pi/pi-tui/theme/theme"; import type { Settings } from "@oh-my-pi/pi-coding-agent"; import type { WebThemeConfig, WebThemePalette } from "@/lib/settings-api"; +import { getSetting } from "@/lib/omp-settings"; function firstColor(...values: Array): string { return values.find((value) => typeof value === "string" && value.length > 0) ?? "transparent"; @@ -111,8 +112,8 @@ export async function getWebThemePalette(name: string): Promise } export async function getWebThemeConfig(settings: Settings): Promise { - const dark = settings.get("theme.dark") ?? "titanium"; - const light = settings.get("theme.light") ?? "light"; + const dark = getSetting(settings, "theme.dark") ?? "titanium"; + const light = getSetting(settings, "theme.light") ?? "light"; const [darkPalette, lightPalette] = await Promise.all([ getWebThemePalette(dark), getWebThemePalette(light), diff --git a/lib/omp-types.ts b/lib/omp-types.ts index d8b1ab2a22..2c55e51497 100644 --- a/lib/omp-types.ts +++ b/lib/omp-types.ts @@ -5,7 +5,8 @@ import type { SlashCommandInfo as OmpSlashCommandInfo, Theme, } from "@oh-my-pi/pi-coding-agent"; -import type { Goal, GoalModeState } from "@oh-my-pi/pi-coding-agent/goals/state"; +import type { GoalModeState } from "@oh-my-pi/pi-coding-agent/goals/state"; +import type { Goal } from "@oh-my-pi/pi-tui/tools/goal"; import type { ExtensionAskDialogQuestion, ExtensionAskDialogResult } from "./types"; diff --git a/lib/provider-api-key-route.test.mjs b/lib/provider-api-key-route.test.mjs index 6be3f5cae1..3d7957e69c 100644 --- a/lib/provider-api-key-route.test.mjs +++ b/lib/provider-api-key-route.test.mjs @@ -9,8 +9,8 @@ test("API key saves go through omp's AuthStorage, not a network catalog refresh" // CLI's store and lock, and avoids the unbounded catalog refresh a login() // call would trigger. assert.doesNotMatch(source, /\.login\(/); - assert.match(source, /authStorage\.set\(provider, \{ type: "api_key"/); - assert.match(source, /authStorage\.remove\(provider\)/); + assert.match(source, /authStorage\.credentials\.set\(provider, \{ type: "api_key"/); + assert.match(source, /authStorage\.credentials\.remove\(provider\)/); }); test("the API key status endpoint never returns the stored key", async () => { @@ -18,5 +18,5 @@ test("the API key status endpoint never returns the stored key", async () => { const getSource = source.slice(source.indexOf("export async function GET"), source.indexOf("export async function POST")); assert.doesNotMatch(getSource, /getApiKey|peekApiKey|credential\.key/); - assert.match(getSource, /configured: authStorage\.hasAuth\(provider\)/); + assert.match(getSource, /configured: authStorage\.keys\.source\(provider\) !== undefined/); }); diff --git a/lib/provider-listing-runtime.ts b/lib/provider-listing-runtime.ts index 80a76871b1..0843109750 100644 --- a/lib/provider-listing-runtime.ts +++ b/lib/provider-listing-runtime.ts @@ -42,7 +42,7 @@ export async function collectProviderListingInputs(): Promise(); for (const id of ids) { - const stored = authStorage.listStoredCredentials(id); + const stored = authStorage.credentials.list(id); const type: ProviderCredentialType | undefined = stored.some((entry) => entry.credential.type === "oauth") ? "oauth" : stored.length > 0 @@ -53,7 +53,7 @@ export async function collectProviderListingInputs(): Promise { const oauth = oauthByProvider.get(id); - const origin = authStorage.getCredentialOrigin(id); + const origin = authStorage.keys.source(id); return { id, name: oauth?.name ?? id, @@ -62,7 +62,7 @@ export async function collectProviderListingInputs(): Promise(settings, "enabledModels"), settings); const defaultRole = readDefaultModelRole(settings); const hasExistingMessages = sessionManager.buildSessionContext().messages.length > 0; const initial = hasExistingMessages diff --git a/lib/runtime.test.mjs b/lib/runtime.test.mjs index 6170f05959..9e969256fd 100644 --- a/lib/runtime.test.mjs +++ b/lib/runtime.test.mjs @@ -74,3 +74,17 @@ test("prefers OMP_WEB_BUN over PATH lookups", () => { rmSync(dir, { recursive: true, force: true }); } }); + +test("adds loopback to NO_PROXY only when a proxy is configured", () => { + const { withLoopbackNoProxy } = require("../bin/runtime.js"); + + const plain = { PATH: "/bin" }; + assert.equal(withLoopbackNoProxy(plain), plain); + + const proxied = withLoopbackNoProxy({ HTTPS_PROXY: "http://proxy:3128", NO_PROXY: "corp.internal, localhost" }); + assert.equal(proxied.NO_PROXY, "corp.internal,localhost,127.0.0.1,::1"); + assert.equal(proxied.no_proxy, proxied.NO_PROXY); + + const wildcard = { HTTP_PROXY: "http://proxy:3128", NO_PROXY: "*" }; + assert.equal(withLoopbackNoProxy(wildcard), wildcard); +}); diff --git a/lib/skills-service.ts b/lib/skills-service.ts index b456f6a41e..888a578bad 100644 --- a/lib/skills-service.ts +++ b/lib/skills-service.ts @@ -5,6 +5,7 @@ import type { SkillInfo, SkillsResponse } from "@/lib/api-types"; import { annotateSkillsWithInstallInfo } from "@/lib/skill-lock"; import { getProjectTrustStatus } from "@/lib/project-trust"; import { getSettingsForCwd } from "@/lib/omp-runtime"; +import { getSkillsSettings } from "@/lib/omp-settings"; /** * Skills exactly as an omp session would see them. @@ -16,7 +17,7 @@ import { getSettingsForCwd } from "@/lib/omp-runtime"; export async function loadSkillsWithInstallInfo(cwd: string): Promise { const agentDir = getAgentDir(); const settings = await getSettingsForCwd(cwd); - const { skills, warnings } = await loadSkills({ cwd, ...settings.getGroup("skills") }); + const { skills, warnings } = await loadSkills({ cwd, ...getSkillsSettings(settings) }); const infos: SkillInfo[] = skills.map((skill) => ({ name: skill.name, diff --git a/lib/startup-preferences.test.mjs b/lib/startup-preferences.test.mjs index 3d7959fcc1..ccec598871 100644 --- a/lib/startup-preferences.test.mjs +++ b/lib/startup-preferences.test.mjs @@ -4,6 +4,8 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { Settings } from "@oh-my-pi/pi-coding-agent"; +import { lookup } from "@oh-my-pi/pi-coding-agent/config/registry"; +import "@oh-my-pi/pi-coding-agent/config/all-settings"; import { createJiti } from "jiti"; const { persistExplicitStartupPreferences } = await createJiti(import.meta.url) @@ -40,7 +42,7 @@ test("stores an explicit browser model as omp's default model role", async () => assert.equal(result.modelDefaultChanged, true); assert.equal(settings.getModelRole("default"), "deepseek/deepseek-chat"); - assert.equal(settings.get("defaultThinkingLevel"), "high"); + assert.equal(lookup("defaultThinkingLevel").get(settings), "high"); }); }); diff --git a/lib/startup-preferences.ts b/lib/startup-preferences.ts index 973ccef3e0..7d0ca03269 100644 --- a/lib/startup-preferences.ts +++ b/lib/startup-preferences.ts @@ -1,7 +1,8 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core"; -import type { ConfiguredThinkingLevel } from "@oh-my-pi/pi-coding-agent/thinking"; +import type { ConfiguredThinkingLevel } from "@oh-my-pi/pi-tui/thinking"; import type { Settings } from "@oh-my-pi/pi-coding-agent"; import { formatRoleSelector } from "./model-roles"; +import { setSetting } from "@/lib/omp-settings"; export interface ExplicitStartupPreferences { model?: { provider: string; modelId: string }; @@ -50,7 +51,7 @@ export async function persistExplicitStartupPreferences( explicit.thinkingLevel && (explicit.thinkingLevel === "auto" || effective.supportsThinking || effective.thinkingLevel !== "off") ) { - settings.set("defaultThinkingLevel", (explicit.thinkingLevel === "auto" ? "auto" : effective.thinkingLevel) as never); + setSetting(settings, "defaultThinkingLevel", explicit.thinkingLevel === "auto" ? "auto" : effective.thinkingLevel); } await settings.flush(); diff --git a/package.json b/package.json index c3583c364f..4ab1f3215a 100644 --- a/package.json +++ b/package.json @@ -52,11 +52,11 @@ "tauri": "tauri" }, "dependencies": { - "@oh-my-pi/pi-agent-core": "~18.1.17", - "@oh-my-pi/pi-ai": "~18.1.17", - "@oh-my-pi/pi-coding-agent": "~18.1.17", - "@oh-my-pi/pi-tui": "~18.1.17", - "@oh-my-pi/pi-utils": "~18.1.17", + "@oh-my-pi/pi-agent-core": "~18.3.2", + "@oh-my-pi/pi-ai": "~18.3.2", + "@oh-my-pi/pi-coding-agent": "~18.3.2", + "@oh-my-pi/pi-tui": "~18.3.2", + "@oh-my-pi/pi-utils": "~18.3.2", "js-yaml": "^5.4.1", "next": "16.2.12", "proper-lockfile": "4.1.2", diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index bbd66a39bc..f155e02d07 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -161,6 +161,9 @@ fn start_server(app: &mut tauri::App) -> Result<(), String> { ) .stdout(Stdio::piped()) .stderr(Stdio::piped()); + if let Some(no_proxy) = loopback_no_proxy() { + command.env("NO_PROXY", &no_proxy).env("no_proxy", &no_proxy); + } make_process_group(&mut command); let mut child = command @@ -304,3 +307,34 @@ fn kill_process_group(pid: u32) { .spawn(); } } + +/// Bun 1.4 sends loopback requests through `HTTP_PROXY` as well, which breaks +/// local providers behind a proxy; it honors `NO_PROXY`, read once at startup, +/// so the loopback hosts are appended before Bun is spawned (bin/runtime.js +/// does the same for the npm launcher). +fn loopback_no_proxy() -> Option { + let proxied = ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy"] + .iter() + .any(|name| std::env::var_os(name).is_some_and(|value| !value.is_empty())); + if !proxied { + return None; + } + let current = std::env::var("NO_PROXY") + .or_else(|_| std::env::var("no_proxy")) + .unwrap_or_default(); + if current.trim() == "*" { + return None; + } + let mut entries: Vec = current + .split(',') + .map(|entry| entry.trim().to_string()) + .filter(|entry| !entry.is_empty()) + .collect(); + let before = entries.len(); + for host in ["localhost", "127.0.0.1", "::1"] { + if !entries.iter().any(|entry| entry == host) { + entries.push(host.to_string()); + } + } + (entries.len() != before).then(|| entries.join(",")) +}