mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-16 22:32:42 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df081a06ed | ||
|
|
c6a4ae81c9 |
Generated
-10
@@ -39240,7 +39240,6 @@
|
||||
"strip-ansi": "^7.1.2",
|
||||
"tree-kill": "^1.2.2",
|
||||
"typescript": "^5.9.3",
|
||||
"undici": "^7.30.0",
|
||||
"uuid": "^9.0.1",
|
||||
"which": "^5.0.0",
|
||||
"ws": "^8.14.2",
|
||||
@@ -39971,15 +39970,6 @@
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"packages/server/node_modules/undici": {
|
||||
"version": "7.30.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz",
|
||||
"integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.18.1"
|
||||
}
|
||||
},
|
||||
"packages/server/node_modules/vitest": {
|
||||
"version": "4.1.6",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz",
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
expectConflictPreservesDraft,
|
||||
discardTitle,
|
||||
returnAndReopenSettings,
|
||||
returnToPluginsPage,
|
||||
expectSettingsUnavailable,
|
||||
} from "../support/helpers/plugin-settings";
|
||||
|
||||
@@ -44,11 +43,6 @@ test("another client's save preserves a conflicting draft", async ({ page }) =>
|
||||
await discardTitle(page);
|
||||
});
|
||||
|
||||
test("Back to plugins returns from a plugin settings screen", async ({ page }) => {
|
||||
await openDisplaySettings(page);
|
||||
await returnToPluginsPage(page);
|
||||
});
|
||||
|
||||
test.describe("compact plugin settings", () => {
|
||||
test.use({ viewport: { width: 390, height: 844 } });
|
||||
|
||||
|
||||
@@ -3,8 +3,6 @@ import { expect, test as base, type Page } from "../fixtures";
|
||||
import { gotoAppShell, openSettings } from "./app";
|
||||
import { goBackInSettings, openCompactSettings, openHostSection } from "./settings";
|
||||
import { getServerId } from "./server-id";
|
||||
import { expectAppRoute } from "./route-assertions";
|
||||
import { buildSettingsHostSectionRoute } from "@/utils/host-routes";
|
||||
import { connectNewWorkspaceDaemonClient } from "./new-workspace";
|
||||
|
||||
type SettingsClient = Awaited<ReturnType<typeof connectNewWorkspaceDaemonClient>>;
|
||||
@@ -107,13 +105,6 @@ export async function returnAndReopenSettings(page: Page) {
|
||||
await page.getByRole("menuitem", { name: "Display", exact: true }).click();
|
||||
await expect(page.getByRole("switch", { name: "Show metadata" })).toBeVisible();
|
||||
}
|
||||
export async function returnToPluginsPage(page: Page) {
|
||||
await page.getByRole("button", { name: "Back to plugins", exact: true }).click();
|
||||
await expectAppRoute(page, buildSettingsHostSectionRoute(getServerId(), "plugins"));
|
||||
await expect(
|
||||
page.getByRole("button", { name: "Actions for settings-example", exact: true }),
|
||||
).toBeVisible();
|
||||
}
|
||||
export async function expectSettingsUnavailable(page: Page) {
|
||||
await expect(
|
||||
page.getByText("This plugin settings screen is unavailable.", { exact: true }),
|
||||
|
||||
@@ -2071,7 +2071,6 @@ export const en = {
|
||||
offline: "Connect to this host to open plugin settings.",
|
||||
update: "Update this host to use plugin settings.",
|
||||
unavailable: "This plugin settings screen is unavailable.",
|
||||
backToPlugins: "Back to plugins",
|
||||
},
|
||||
trustedTitle: "Plugins are trusted code",
|
||||
trustedDescription:
|
||||
|
||||
@@ -18,7 +18,6 @@ export const pluginSettings = {
|
||||
offline: "اتصل بهذا المضيف لفتح إعدادات الإضافة.",
|
||||
update: "حدّث هذا المضيف لاستخدام إعدادات الإضافة.",
|
||||
unavailable: "شاشة إعدادات الإضافة هذه غير متاحة.",
|
||||
backToPlugins: "العودة إلى الإضافات",
|
||||
},
|
||||
title: "الإضافات",
|
||||
trustedTitle: "الإضافات تعليمات برمجية موثوقة",
|
||||
@@ -73,7 +72,6 @@ export const pluginSettings = {
|
||||
offline: "Conéctate a este host para abrir los ajustes del plugin.",
|
||||
update: "Actualiza este host para usar los ajustes del plugin.",
|
||||
unavailable: "Esta pantalla de ajustes del plugin no está disponible.",
|
||||
backToPlugins: "Volver a plugins",
|
||||
},
|
||||
title: "Plugins",
|
||||
trustedTitle: "Los plugins son código de confianza",
|
||||
@@ -128,7 +126,6 @@ export const pluginSettings = {
|
||||
offline: "Connectez-vous à cet hôte pour ouvrir les paramètres du plugin.",
|
||||
update: "Mettez à jour cet hôte pour utiliser les paramètres du plugin.",
|
||||
unavailable: "Cet écran de paramètres du plugin est indisponible.",
|
||||
backToPlugins: "Retour aux plugins",
|
||||
},
|
||||
title: "Plugins",
|
||||
trustedTitle: "Les plugins sont du code de confiance",
|
||||
@@ -183,7 +180,6 @@ export const pluginSettings = {
|
||||
offline: "このホストに接続してプラグイン設定を開いてください。",
|
||||
update: "プラグイン設定を使用するには、このホストを更新してください。",
|
||||
unavailable: "このプラグイン設定画面は利用できません。",
|
||||
backToPlugins: "プラグインに戻る",
|
||||
},
|
||||
title: "プラグイン",
|
||||
trustedTitle: "プラグインは信頼済みコードです",
|
||||
@@ -238,7 +234,6 @@ export const pluginSettings = {
|
||||
offline: "플러그인 설정을 열려면 이 호스트에 연결하세요.",
|
||||
update: "플러그인 설정을 사용하려면 이 호스트를 업데이트하세요.",
|
||||
unavailable: "이 플러그인 설정 화면을 사용할 수 없습니다.",
|
||||
backToPlugins: "플러그인으로 돌아가기",
|
||||
},
|
||||
title: "플러그인",
|
||||
trustedTitle: "플러그인은 신뢰하는 코드입니다",
|
||||
@@ -292,7 +287,6 @@ export const pluginSettings = {
|
||||
offline: "Conecte-se a este host para abrir as configurações do plugin.",
|
||||
update: "Atualize este host para usar as configurações do plugin.",
|
||||
unavailable: "Esta tela de configurações do plugin está indisponível.",
|
||||
backToPlugins: "Voltar para plugins",
|
||||
},
|
||||
title: "Plugins",
|
||||
trustedTitle: "Plugins são código confiável",
|
||||
@@ -347,7 +341,6 @@ export const pluginSettings = {
|
||||
offline: "Подключитесь к этому хосту, чтобы открыть настройки плагина.",
|
||||
update: "Обновите этот хост, чтобы использовать настройки плагина.",
|
||||
unavailable: "Этот экран настроек плагина недоступен.",
|
||||
backToPlugins: "Вернуться к плагинам",
|
||||
},
|
||||
title: "Плагины",
|
||||
trustedTitle: "Плагины — доверенный код",
|
||||
@@ -402,7 +395,6 @@ export const pluginSettings = {
|
||||
offline: "连接到此主机以打开插件设置。",
|
||||
update: "更新此主机以使用插件设置。",
|
||||
unavailable: "此插件设置页面不可用。",
|
||||
backToPlugins: "返回插件",
|
||||
},
|
||||
title: "插件",
|
||||
trustedTitle: "插件是受信任的代码",
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useTranslation } from "react-i18next";
|
||||
import { Platform, Text, View } from "react-native";
|
||||
import { StyleSheet, withUnistyles } from "react-native-unistyles";
|
||||
import { router } from "expo-router";
|
||||
import { ArrowLeft } from "lucide-react-native";
|
||||
import type { PluginHostProps } from "@getpaseo/plugin/client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DropdownMenuItem } from "@/components/ui/dropdown-menu";
|
||||
@@ -128,30 +127,7 @@ function SettingsContent({
|
||||
}
|
||||
const ThemedSettingsContent = withUnistyles(SettingsContent);
|
||||
const themeMapping = (theme: Theme) => ({ theme: toPluginTheme(theme) });
|
||||
export function PluginSettingsContent({
|
||||
onBackToPlugins,
|
||||
showBackToPlugins,
|
||||
...identity
|
||||
}: SettingsIdentity & { onBackToPlugins: () => void; showBackToPlugins: boolean }) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<View>
|
||||
{showBackToPlugins ? (
|
||||
<Button
|
||||
onPress={onBackToPlugins}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
leftIcon={ArrowLeft}
|
||||
style={styles.backButton}
|
||||
>
|
||||
{t("settings.plugins.screens.backToPlugins")}
|
||||
</Button>
|
||||
) : null}
|
||||
<ThemedSettingsContent {...identity} uniProps={themeMapping} />
|
||||
</View>
|
||||
);
|
||||
export function PluginSettingsContent(props: SettingsIdentity) {
|
||||
return <ThemedSettingsContent {...props} uniProps={themeMapping} />;
|
||||
}
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
message: { color: theme.colors.foregroundMuted },
|
||||
backButton: { alignSelf: "flex-start", paddingHorizontal: 0, marginBottom: theme.spacing[4] },
|
||||
}));
|
||||
const styles = StyleSheet.create((theme) => ({ message: { color: theme.colors.foregroundMuted } }));
|
||||
|
||||
@@ -1333,8 +1333,6 @@ export default function SettingsScreen({ view, openAddHostIntent = null }: Setti
|
||||
serverId={view.serverId}
|
||||
pluginId={view.pluginId}
|
||||
screenId={view.screenId}
|
||||
onBackToPlugins={handleBackFromDetail}
|
||||
showBackToPlugins={!isCompactLayout}
|
||||
/>
|
||||
);
|
||||
if (view.kind === "host") {
|
||||
|
||||
@@ -141,7 +141,6 @@
|
||||
"strip-ansi": "^7.1.2",
|
||||
"tree-kill": "^1.2.2",
|
||||
"typescript": "^5.9.3",
|
||||
"undici": "^7.30.0",
|
||||
"uuid": "^9.0.1",
|
||||
"which": "^5.0.0",
|
||||
"ws": "^8.14.2",
|
||||
|
||||
@@ -1117,6 +1117,42 @@ describe("Codex app-server provider", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("starts a session without a model on the project's Codex model and effort", async () => {
|
||||
const cwd = "/workspace/project";
|
||||
const appServer = createFakeCodexAppServer({
|
||||
"config/read": (params) => ({
|
||||
config:
|
||||
(params as { cwd?: unknown }).cwd === cwd
|
||||
? { model: "project-model", model_reasoning_effort: "high" }
|
||||
: { model: "global-model", model_reasoning_effort: "low" },
|
||||
}),
|
||||
getUserSavedConfig: () => ({
|
||||
config: { model: "global-model", modelReasoningEffort: "low" },
|
||||
}),
|
||||
});
|
||||
const session = new CodexAppServerAgentSession(
|
||||
createConfig({ cwd, model: undefined }),
|
||||
null,
|
||||
createTestLogger(),
|
||||
async () => appServer.child,
|
||||
);
|
||||
|
||||
try {
|
||||
await session.startTurn("use project defaults");
|
||||
|
||||
await expect(appServer.waitForRequest("thread/start")).resolves.toMatchObject({
|
||||
model: "project-model",
|
||||
});
|
||||
await expect(appServer.waitForTurnStart()).resolves.toMatchObject({
|
||||
model: "project-model",
|
||||
effort: "high",
|
||||
});
|
||||
appServer.assertNoErrors();
|
||||
} finally {
|
||||
await session.close();
|
||||
}
|
||||
});
|
||||
|
||||
test("preserves cwd-resolved Codex writable roots under an explicit workflow mode", async () => {
|
||||
const appServer = createFakeCodexAppServer({
|
||||
"config/read": () => ({
|
||||
|
||||
@@ -3100,10 +3100,32 @@ const CodexNotificationSchema = z.union([
|
||||
),
|
||||
]);
|
||||
|
||||
// config/read resolves the effective layered config, including a trusted project's
|
||||
// .codex/config.toml when given its cwd; getUserSavedConfig predates it.
|
||||
async function readCodexConfiguredDefaults(
|
||||
client: CodexAppServerClient,
|
||||
logger: Logger,
|
||||
cwd: string | null,
|
||||
): Promise<CodexConfiguredDefaults> {
|
||||
let configReadDefaults: CodexConfiguredDefaults = {};
|
||||
try {
|
||||
const response = toObjectRecord(await client.request("config/read", { cwd }));
|
||||
const config = toObjectRecord(response?.config);
|
||||
const modelValue = typeof config?.model === "string" ? config.model : undefined;
|
||||
const thinkingOptionValue =
|
||||
typeof config?.model_reasoning_effort === "string" ? config.model_reasoning_effort : null;
|
||||
configReadDefaults = {
|
||||
model: normalizeCodexModelId(modelValue),
|
||||
thinkingOptionId: normalizeCodexThinkingOptionId(thinkingOptionValue),
|
||||
};
|
||||
} catch (error) {
|
||||
logger.debug({ error }, "Failed to read Codex config defaults");
|
||||
}
|
||||
|
||||
if (configReadDefaults.model && configReadDefaults.thinkingOptionId) {
|
||||
return configReadDefaults;
|
||||
}
|
||||
|
||||
let savedConfigDefaults: CodexConfiguredDefaults = {};
|
||||
try {
|
||||
const response = toObjectRecord(await client.request("getUserSavedConfig", {}));
|
||||
@@ -3119,26 +3141,7 @@ async function readCodexConfiguredDefaults(
|
||||
logger.debug({ error }, "Failed to read Codex saved config defaults");
|
||||
}
|
||||
|
||||
if (savedConfigDefaults.model && savedConfigDefaults.thinkingOptionId) {
|
||||
return savedConfigDefaults;
|
||||
}
|
||||
|
||||
let configReadDefaults: CodexConfiguredDefaults = {};
|
||||
try {
|
||||
const response = toObjectRecord(await client.request("config/read", {}));
|
||||
const config = toObjectRecord(response?.config);
|
||||
const modelValue = typeof config?.model === "string" ? config.model : undefined;
|
||||
const thinkingOptionValue =
|
||||
typeof config?.model_reasoning_effort === "string" ? config.model_reasoning_effort : null;
|
||||
configReadDefaults = {
|
||||
model: normalizeCodexModelId(modelValue),
|
||||
thinkingOptionId: normalizeCodexThinkingOptionId(thinkingOptionValue),
|
||||
};
|
||||
} catch (error) {
|
||||
logger.debug({ error }, "Failed to read Codex config defaults");
|
||||
}
|
||||
|
||||
return mergeCodexConfiguredDefaults(savedConfigDefaults, configReadDefaults);
|
||||
return mergeCodexConfiguredDefaults(configReadDefaults, savedConfigDefaults);
|
||||
}
|
||||
|
||||
interface CodexSkillPromptBlock {
|
||||
@@ -5119,7 +5122,11 @@ export class CodexAppServerAgentSession implements AgentSession {
|
||||
let model = this.config.model;
|
||||
let thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId);
|
||||
if (!model || !thinkingOptionId) {
|
||||
configuredDefaults = await readCodexConfiguredDefaults(this.client, this.logger);
|
||||
configuredDefaults = await readCodexConfiguredDefaults(
|
||||
this.client,
|
||||
this.logger,
|
||||
this.config.cwd ?? null,
|
||||
);
|
||||
}
|
||||
if (!model) {
|
||||
model = configuredDefaults.model;
|
||||
@@ -7323,7 +7330,7 @@ export class CodexAppServerAgentClient implements AgentClient {
|
||||
const parsedResponse = CodexModelListResponseSchema.safeParse(rawResponse);
|
||||
const models = parsedResponse.success ? (parsedResponse.data.data ?? []) : [];
|
||||
const configuredDefaults = await runProviderRefreshActivity(context, "config/read", () =>
|
||||
readCodexConfiguredDefaults(client!, this.logger),
|
||||
readCodexConfiguredDefaults(client!, this.logger, null),
|
||||
);
|
||||
const configuredDefaultModelId = configuredDefaults.model;
|
||||
const configuredDefaultThinkingOptionId = configuredDefaults.thinkingOptionId;
|
||||
|
||||
@@ -355,7 +355,8 @@ test.each([
|
||||
await resumed?.close();
|
||||
await original?.close();
|
||||
await client.shutdown();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
// OpenCode can still hold its ripgrep download open on Windows right after shutdown.
|
||||
await rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 });
|
||||
}
|
||||
},
|
||||
240_000,
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { Agent, getGlobalDispatcher, setGlobalDispatcher } from "undici";
|
||||
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
||||
|
||||
import { createTestLogger } from "../../../../../test-utils/test-logger.js";
|
||||
import { V2Runtime } from "./runtime.js";
|
||||
|
||||
// Answers every request at once except session.wait, which it holds for WAIT_MS before answering,
|
||||
// the way OpenCode holds the wait response until the session's agent loop is idle.
|
||||
const FAKE_HELPER = `
|
||||
import http from "node:http";
|
||||
const server = http.createServer((request, response) => {
|
||||
if (request.url.endsWith("/wait")) {
|
||||
setTimeout(() => { response.statusCode = 204; response.end(); }, Number(process.env.WAIT_MS));
|
||||
return;
|
||||
}
|
||||
response.setHeader("content-type", "application/json");
|
||||
response.end("{}");
|
||||
});
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
console.log("server listening on http://127.0.0.1:" + server.address().port);
|
||||
});
|
||||
`;
|
||||
|
||||
let root: string;
|
||||
const defaultDispatcher = getGlobalDispatcher();
|
||||
|
||||
beforeEach(async () => {
|
||||
root = await mkdtemp(path.join(os.tmpdir(), "paseo-opencode-v2-runtime-"));
|
||||
vi.stubEnv("PASEO_HOME", path.join(root, "home"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
setGlobalDispatcher(defaultDispatcher);
|
||||
vi.unstubAllEnvs();
|
||||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("session.wait outlasts the fetch header deadline until the helper reports the session idle", async () => {
|
||||
// Stands in for undici's five-minute default, which ended every longer turn.
|
||||
setGlobalDispatcher(new Agent({ headersTimeout: 200 }));
|
||||
const helper = path.join(root, "fake-opencode.mjs");
|
||||
await writeFile(helper, FAKE_HELPER);
|
||||
const runtime = new V2Runtime({
|
||||
logger: createTestLogger(),
|
||||
settings: {
|
||||
command: { mode: "replace", argv: [process.execPath, helper] },
|
||||
env: { WAIT_MS: "2500" },
|
||||
},
|
||||
decorateEnv: async (env) => env,
|
||||
});
|
||||
try {
|
||||
const connection = await runtime.acquire();
|
||||
await expect(connection.client.session.wait({ sessionID: "ses_long" })).resolves.toBe(
|
||||
undefined,
|
||||
);
|
||||
} finally {
|
||||
await runtime.shutdown();
|
||||
}
|
||||
});
|
||||
@@ -5,7 +5,6 @@ import type { V2Api } from "./api.js";
|
||||
import { randomBytes } from "node:crypto";
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import type { Logger } from "pino";
|
||||
import { Agent } from "undici";
|
||||
import { spawnProcess } from "../../../../../utils/spawn.js";
|
||||
import { terminateWithTreeKill } from "../../../../../utils/tree-kill.js";
|
||||
import {
|
||||
@@ -175,9 +174,6 @@ export class V2Runtime {
|
||||
}),
|
||||
});
|
||||
const processAbort = new AbortController();
|
||||
// session.wait answers only once the session is idle, so a header deadline would fail any
|
||||
// turn that outlasts it. Requests end when the helper exits or the caller aborts instead.
|
||||
const dispatcher = new Agent({ headersTimeout: 0 });
|
||||
const exited = new Promise<Error>((resolve) =>
|
||||
process.once("exit", (code) => {
|
||||
const error = new Error(`OpenCode helper server exited (${code})`);
|
||||
@@ -203,7 +199,6 @@ export class V2Runtime {
|
||||
const stop = () => {
|
||||
stopped ??= (async () => {
|
||||
await terminateWithTreeKill(process, { gracefulTimeoutMs: 5_000, forceTimeoutMs: 1_000 });
|
||||
await dispatcher.destroy();
|
||||
const entry = await record;
|
||||
if (entry) await managedProcesses?.remove(entry.id);
|
||||
})();
|
||||
@@ -248,9 +243,7 @@ export class V2Runtime {
|
||||
const signal = init?.signal
|
||||
? AbortSignal.any([init.signal, processAbort.signal])
|
||||
: processAbort.signal;
|
||||
// Node's fetch accepts an undici dispatcher, which the DOM RequestInit type omits.
|
||||
const requestInit: RequestInit & { dispatcher: Agent } = { ...init, signal, dispatcher };
|
||||
const response = await fetch(request, requestInit);
|
||||
const response = await fetch(request, { ...init, signal });
|
||||
const html = response.headers.get("content-type")?.includes("text/html") ?? false;
|
||||
if (!response.ok || html) {
|
||||
const requestUrl = request instanceof Request ? request.url : String(request);
|
||||
|
||||
Reference in New Issue
Block a user