Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,4 @@ next-env.d.ts
src-tauri/target/
src-tauri/gen/
src-tauri/resources/
src-tauri/server/
src-tauri/server/.remember/
2 changes: 1 addition & 1 deletion app/api/agent/new/route.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
10 changes: 5 additions & 5 deletions app/api/auth/api-key/[provider]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand All @@ -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) {
Expand All @@ -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 });
Expand Down
2 changes: 1 addition & 1 deletion app/api/auth/login/[provider]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
4 changes: 2 additions & 2 deletions app/api/auth/logout/[provider]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
3 changes: 2 additions & 1 deletion app/api/display-settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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) {
Expand Down
5 changes: 3 additions & 2 deletions app/api/model-roles/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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<string[]>(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 });
Expand Down Expand Up @@ -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<string[]>(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 });
Expand Down
3 changes: 2 additions & 1 deletion app/api/models/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -35,7 +36,7 @@ async function loadModels(cwd: string): Promise<ModelsData> {
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<string[]>(settings, "enabledModels"), settings);
const { visible, thinkingLevelPins, warnings } = scope;
const modelList = visible.map((m) => ({
id: m.id,
Expand Down
17 changes: 10 additions & 7 deletions app/api/settings/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand All @@ -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 });
}
Expand Down
3 changes: 2 additions & 1 deletion bin/omp-web.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const {
isBunVersionSupported,
isNodeVersionSupported,
resolveBunPath,
withLoopbackNoProxy,
// eslint-disable-next-line @typescript-eslint/no-require-imports
} = require("./runtime");

Expand Down Expand Up @@ -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
Expand Down
21 changes: 21 additions & 0 deletions bin/runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -113,4 +133,5 @@ module.exports = {
isBunVersionSupported,
isNodeVersionSupported,
resolveBunPath,
withLoopbackNoProxy,
};
Loading