mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 18:52:39 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df081a06ed | ||
|
|
c6a4ae81c9 | ||
|
|
d91171954b | ||
|
|
f232d23e9a | ||
|
|
68e722ec2e |
@@ -17,12 +17,14 @@ import {
|
|||||||
hoverChatOutlinePrompt,
|
hoverChatOutlinePrompt,
|
||||||
chatOutlineRail,
|
chatOutlineRail,
|
||||||
movePointerOffChatOutline,
|
movePointerOffChatOutline,
|
||||||
|
observePromptIndexRequests,
|
||||||
pointAtChatOutlineRowEdge,
|
pointAtChatOutlineRowEdge,
|
||||||
pressEnterOnFocusedPrompt,
|
pressEnterOnFocusedPrompt,
|
||||||
splitCurrentPanelRight,
|
splitCurrentPanelRight,
|
||||||
withStreamingMarkdownOutline,
|
withStreamingMarkdownOutline,
|
||||||
expectReadingStreamedMarkdown,
|
expectReadingStreamedMarkdown,
|
||||||
} from "../support/helpers/chat-outline";
|
} from "../support/helpers/chat-outline";
|
||||||
|
import { createCreationScenario } from "../support/helpers/creation";
|
||||||
import {
|
import {
|
||||||
expectTimelineAtMaximumScrollWithPromptVisible,
|
expectTimelineAtMaximumScrollWithPromptVisible,
|
||||||
expectTimelinePromptNotMounted,
|
expectTimelinePromptNotMounted,
|
||||||
@@ -266,3 +268,21 @@ test.describe("desktop chat outline", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("indexes a new agent's prompts only once the agent exists", async ({ page }) => {
|
||||||
|
const promptIndex = await observePromptIndexRequests(page);
|
||||||
|
const creation = await createCreationScenario(page);
|
||||||
|
try {
|
||||||
|
await page.setViewportSize(WIDE_VIEWPORT);
|
||||||
|
await creation.openAgentDraft();
|
||||||
|
await creation.submitPrompt("Start an agent with an outline.");
|
||||||
|
await creation.expectPromptVisible();
|
||||||
|
|
||||||
|
const agentIds = await creation.agentIds();
|
||||||
|
expect(agentIds).toHaveLength(1);
|
||||||
|
await promptIndex.waitForRequestFor(agentIds[0]!);
|
||||||
|
expect(promptIndex.requestedAgentIds()).toEqual(agentIds);
|
||||||
|
} finally {
|
||||||
|
await creation.cleanup();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { openSettings } from "./app";
|
|||||||
import { openSettingsSection } from "./settings";
|
import { openSettingsSection } from "./settings";
|
||||||
import { runWorkspaceActionFromCommandCenter } from "./command-center-workspace-actions";
|
import { runWorkspaceActionFromCommandCenter } from "./command-center-workspace-actions";
|
||||||
import { seedMockAgentWorkspace, type MockAgentWorkspace } from "./mock-agent";
|
import { seedMockAgentWorkspace, type MockAgentWorkspace } from "./mock-agent";
|
||||||
|
import { loadSessionMessageReaders } from "./new-workspace";
|
||||||
|
|
||||||
export async function withStreamingMarkdownOutline(
|
export async function withStreamingMarkdownOutline(
|
||||||
run: (agent: MockAgentWorkspace) => Promise<void>,
|
run: (agent: MockAgentWorkspace) => Promise<void>,
|
||||||
@@ -229,3 +230,22 @@ async function requireBoundingBox(
|
|||||||
}
|
}
|
||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function observePromptIndexRequests(page: Page) {
|
||||||
|
const frames = await loadSessionMessageReaders();
|
||||||
|
const agentIds: string[] = [];
|
||||||
|
page.on("websocket", (socket) => {
|
||||||
|
socket.on("framesent", ({ payload }) => {
|
||||||
|
const request = frames.client(payload);
|
||||||
|
if (request?.type === "agent.timeline.list_prompts.request") {
|
||||||
|
agentIds.push(request.agentId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
async waitForRequestFor(agentId: string) {
|
||||||
|
await expect.poll(() => agentIds).toContain(agentId);
|
||||||
|
},
|
||||||
|
requestedAgentIds: () => [...new Set(agentIds)],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@@ -206,6 +206,12 @@ export async function createCreationScenario(page: Page) {
|
|||||||
)
|
)
|
||||||
.toBe(count);
|
.toBe(count);
|
||||||
},
|
},
|
||||||
|
async agentIds() {
|
||||||
|
await requests.settled();
|
||||||
|
return (await project.client.fetchAgents()).entries
|
||||||
|
.filter(({ agent }) => agent.workspaceId === workspaceId)
|
||||||
|
.map(({ agent }) => agent.id);
|
||||||
|
},
|
||||||
async expectAgentTitle(title: string) {
|
async expectAgentTitle(title: string) {
|
||||||
await expect
|
await expect
|
||||||
.poll(
|
.poll(
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ describe("chat outline prompt index epoch", () => {
|
|||||||
it("accepts only the authoritative timeline epoch", () => {
|
it("accepts only the authoritative timeline epoch", () => {
|
||||||
expect(shouldAcceptPromptIndexEpoch("epoch-2", "epoch-2")).toBe(true);
|
expect(shouldAcceptPromptIndexEpoch("epoch-2", "epoch-2")).toBe(true);
|
||||||
expect(shouldAcceptPromptIndexEpoch("epoch-2", "epoch-1")).toBe(false);
|
expect(shouldAcceptPromptIndexEpoch("epoch-2", "epoch-1")).toBe(false);
|
||||||
expect(shouldAcceptPromptIndexEpoch(null, "epoch-1")).toBe(true);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,8 @@ import type { AgentTimelinePromptIndexPayload } from "@getpaseo/client/internal/
|
|||||||
|
|
||||||
export type ChatOutlinePrompt = AgentTimelinePromptIndexPayload["prompts"][number];
|
export type ChatOutlinePrompt = AgentTimelinePromptIndexPayload["prompts"][number];
|
||||||
|
|
||||||
export function shouldAcceptPromptIndexEpoch(
|
export function shouldAcceptPromptIndexEpoch(timelineEpoch: string, indexEpoch: string): boolean {
|
||||||
timelineEpoch: string | null,
|
return timelineEpoch === indexEpoch;
|
||||||
indexEpoch: string,
|
|
||||||
): boolean {
|
|
||||||
return timelineEpoch === null || timelineEpoch === indexEpoch;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -37,6 +37,31 @@ describe("useChatOutline", () => {
|
|||||||
runtime.subscribeAgentTimeline.mockClear();
|
runtime.subscribeAgentTimeline.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("waits for a served timeline before requesting its prompt index", async () => {
|
||||||
|
runtime.listAgentTimelinePrompts.mockResolvedValue({ epoch: "epoch-1", prompts: [] });
|
||||||
|
const viewportRef = createRef<StreamViewportHandle>();
|
||||||
|
const { rerender } = renderHook(
|
||||||
|
({ agentId, timelineEpoch }) =>
|
||||||
|
useChatOutline({
|
||||||
|
agentId,
|
||||||
|
serverId: "server-1",
|
||||||
|
timelineEpoch,
|
||||||
|
tail: [],
|
||||||
|
head: [],
|
||||||
|
enabled: true,
|
||||||
|
viewportRef,
|
||||||
|
onJumpError: vi.fn(),
|
||||||
|
}),
|
||||||
|
{ initialProps: { agentId: "draft_msg_1", timelineEpoch: null as string | null } },
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(runtime.listAgentTimelinePrompts).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
rerender({ agentId: "agent-1", timelineEpoch: "epoch-1" });
|
||||||
|
await waitFor(() => expect(runtime.listAgentTimelinePrompts).toHaveBeenCalledTimes(1));
|
||||||
|
expect(runtime.listAgentTimelinePrompts).toHaveBeenCalledWith("agent-1");
|
||||||
|
});
|
||||||
|
|
||||||
it("drops a late prompt index after the authoritative timeline epoch changes", async () => {
|
it("drops a late prompt index after the authoritative timeline epoch changes", async () => {
|
||||||
const first = deferred<{ epoch: string; prompts: [] }>();
|
const first = deferred<{ epoch: string; prompts: [] }>();
|
||||||
const second = deferred<{
|
const second = deferred<{
|
||||||
|
|||||||
@@ -77,8 +77,10 @@ export function useChatOutline({
|
|||||||
|
|
||||||
useEffect(() => setIndex(null), [agentId, enabled, serverId, timelineEpoch]);
|
useEffect(() => setIndex(null), [agentId, enabled, serverId, timelineEpoch]);
|
||||||
|
|
||||||
|
// Only a timeline the daemon has served can be indexed. A draft's optimistic stream has no
|
||||||
|
// epoch, and its id names no agent the daemon knows.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isWeb || !enabled) {
|
if (!isWeb || !enabled || timelineEpoch === null) {
|
||||||
setIndex(null);
|
setIndex(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,10 +58,23 @@ export async function runFollowUp(url: string, agentId: string): Promise<string
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await client.connect();
|
await client.connect();
|
||||||
const result = await client.agents.ref(agentId).run("Summarize your progress and next step.");
|
const result = await client.agents.ref(agentId).run("Summarize your progress and next step.", {
|
||||||
|
activeTurnBehavior: "interrupt",
|
||||||
|
});
|
||||||
if (result.status !== "idle") throw new Error(result.error ?? result.status);
|
if (result.status !== "idle") throw new Error(result.error ?? result.status);
|
||||||
return result.lastMessage;
|
return result.lastMessage;
|
||||||
} finally {
|
} finally {
|
||||||
await client.close();
|
await client.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function steerAgent(url: string, agentId: string, prompt: string): Promise<void> {
|
||||||
|
const client = createClient(url);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await client.connect();
|
||||||
|
await client.agents.ref(agentId).send(prompt, { activeTurnBehavior: "steer" });
|
||||||
|
} finally {
|
||||||
|
await client.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -425,6 +425,7 @@ export interface DaemonClientTrace {
|
|||||||
|
|
||||||
export interface SendMessageOptions {
|
export interface SendMessageOptions {
|
||||||
messageId?: string;
|
messageId?: string;
|
||||||
|
/** What happens when the agent is mid-turn. The daemon interrupts the turn when omitted. */
|
||||||
activeTurnBehavior?: ActiveTurnBehavior;
|
activeTurnBehavior?: ActiveTurnBehavior;
|
||||||
images?: Array<{ data: string; mimeType: string }>;
|
images?: Array<{ data: string; mimeType: string }>;
|
||||||
attachments?: SendAgentMessageRequest["attachments"];
|
attachments?: SendAgentMessageRequest["attachments"];
|
||||||
|
|||||||
@@ -744,13 +744,17 @@ test("agent handles delegate create, send, timeline refetch, archive, and local
|
|||||||
expect(updatedAgents).toEqual(["Updated"]);
|
expect(updatedAgents).toEqual(["Updated"]);
|
||||||
expect(agent.current()).toEqual(updatedAgent);
|
expect(agent.current()).toEqual(updatedAgent);
|
||||||
|
|
||||||
const sendPromise = agent.send("hello", { messageId: "message-sdk" });
|
const sendPromise = agent.send("hello", {
|
||||||
|
messageId: "message-sdk",
|
||||||
|
activeTurnBehavior: "steer",
|
||||||
|
});
|
||||||
const sendRequest = parseSentSessionMessage(ws.sent.at(-1));
|
const sendRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||||
expect(sendRequest).toMatchObject({
|
expect(sendRequest).toMatchObject({
|
||||||
type: "send_agent_message_request",
|
type: "send_agent_message_request",
|
||||||
agentId: "agent_sdk",
|
agentId: "agent_sdk",
|
||||||
text: "hello",
|
text: "hello",
|
||||||
messageId: "message-sdk",
|
messageId: "message-sdk",
|
||||||
|
activeTurnBehavior: "steer",
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.message(
|
ws.message(
|
||||||
@@ -768,6 +772,7 @@ test("agent handles delegate create, send, timeline refetch, archive, and local
|
|||||||
|
|
||||||
const runPromise = agent.run("finish the task", {
|
const runPromise = agent.run("finish the task", {
|
||||||
messageId: "run-message-sdk",
|
messageId: "run-message-sdk",
|
||||||
|
activeTurnBehavior: "interrupt",
|
||||||
timeoutMs: 30_000,
|
timeoutMs: 30_000,
|
||||||
});
|
});
|
||||||
const runSendRequest = parseSentSessionMessage(ws.sent.at(-1));
|
const runSendRequest = parseSentSessionMessage(ws.sent.at(-1));
|
||||||
@@ -776,6 +781,7 @@ test("agent handles delegate create, send, timeline refetch, archive, and local
|
|||||||
agentId: "agent_sdk",
|
agentId: "agent_sdk",
|
||||||
text: "finish the task",
|
text: "finish the task",
|
||||||
messageId: "run-message-sdk",
|
messageId: "run-message-sdk",
|
||||||
|
activeTurnBehavior: "interrupt",
|
||||||
});
|
});
|
||||||
ws.message(
|
ws.message(
|
||||||
sessionMessage({
|
sessionMessage({
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import type {
|
|||||||
ProjectPlacementPayload,
|
ProjectPlacementPayload,
|
||||||
WorkspaceProjectDescriptorPayload,
|
WorkspaceProjectDescriptorPayload,
|
||||||
RefreshProvidersSnapshotResponseMessage,
|
RefreshProvidersSnapshotResponseMessage,
|
||||||
SendAgentMessageRequest,
|
|
||||||
SessionOutboundMessage,
|
SessionOutboundMessage,
|
||||||
WorkspaceDescriptorPayload,
|
WorkspaceDescriptorPayload,
|
||||||
WorkspaceCreateRequest,
|
WorkspaceCreateRequest,
|
||||||
@@ -55,6 +54,7 @@ import type {
|
|||||||
FetchAgentTimelineDirection,
|
FetchAgentTimelineDirection,
|
||||||
FetchAgentTimelinePayload,
|
FetchAgentTimelinePayload,
|
||||||
FetchAgentTimelineProjection,
|
FetchAgentTimelineProjection,
|
||||||
|
SendMessageOptions,
|
||||||
WaitForFinishResult,
|
WaitForFinishResult,
|
||||||
} from "./daemon-client.js";
|
} from "./daemon-client.js";
|
||||||
|
|
||||||
@@ -276,11 +276,7 @@ export interface PaseoAgentTimelineRefetchOptions {
|
|||||||
requestId?: string;
|
requestId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PaseoAgentSendOptions {
|
export type PaseoAgentSendOptions = SendMessageOptions;
|
||||||
messageId?: string;
|
|
||||||
images?: Array<{ data: string; mimeType: string }>;
|
|
||||||
attachments?: SendAgentMessageRequest["attachments"];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PaseoAgentRunOptions extends PaseoAgentSendOptions {
|
export interface PaseoAgentRunOptions extends PaseoAgentSendOptions {
|
||||||
timeoutMs?: number;
|
timeoutMs?: number;
|
||||||
|
|||||||
@@ -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 () => {
|
test("preserves cwd-resolved Codex writable roots under an explicit workflow mode", async () => {
|
||||||
const appServer = createFakeCodexAppServer({
|
const appServer = createFakeCodexAppServer({
|
||||||
"config/read": () => ({
|
"config/read": () => ({
|
||||||
@@ -4324,69 +4360,6 @@ describe("Codex app-server provider", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("replays a legacy sub-agent that later finished as completed", async () => {
|
|
||||||
const session = createSession();
|
|
||||||
session.client = {
|
|
||||||
request: vi.fn(async (method: string, params: unknown) => {
|
|
||||||
if (method !== "thread/read") {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
if ((params as { threadId?: string }).threadId !== "test-thread") {
|
|
||||||
return { thread: { turns: [] } };
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
thread: {
|
|
||||||
turns: [
|
|
||||||
{
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
type: "collabAgentToolCall",
|
|
||||||
id: "exec-spawn-history",
|
|
||||||
tool: "spawnAgent",
|
|
||||||
status: "completed",
|
|
||||||
prompt: "Research child",
|
|
||||||
receiverThreadIds: ["finished-child-thread"],
|
|
||||||
agentsStates: { "finished-child-thread": { status: "pendingInit" } },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "collabAgentToolCall",
|
|
||||||
id: "exec-wait-history",
|
|
||||||
tool: "wait",
|
|
||||||
status: "completed",
|
|
||||||
receiverThreadIds: ["finished-child-thread"],
|
|
||||||
agentsStates: {
|
|
||||||
"finished-child-thread": { status: "completed", message: "done" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "collabAgentToolCall",
|
|
||||||
id: "exec-close-history",
|
|
||||||
tool: "closeAgent",
|
|
||||||
status: "completed",
|
|
||||||
receiverThreadIds: ["finished-child-thread"],
|
|
||||||
agentsStates: { "finished-child-thread": { status: "completed" } },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
await asInternals(session).loadPersistedHistory(session.client);
|
|
||||||
|
|
||||||
const history: AgentStreamEvent[] = [];
|
|
||||||
for await (const event of session.streamHistory()) {
|
|
||||||
history.push(event);
|
|
||||||
}
|
|
||||||
expect(
|
|
||||||
history.flatMap((event) =>
|
|
||||||
event.type === "provider_subagent" && event.event.type === "upsert" ? [event.event] : [],
|
|
||||||
),
|
|
||||||
).toMatchObject([{ type: "upsert", id: "finished-child-thread", status: "completed" }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("loads mixed legacy and MultiAgentV2 sub-agent history", async () => {
|
test("loads mixed legacy and MultiAgentV2 sub-agent history", async () => {
|
||||||
const session = createSession();
|
const session = createSession();
|
||||||
session.client = {
|
session.client = {
|
||||||
|
|||||||
@@ -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(
|
async function readCodexConfiguredDefaults(
|
||||||
client: CodexAppServerClient,
|
client: CodexAppServerClient,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
|
cwd: string | null,
|
||||||
): Promise<CodexConfiguredDefaults> {
|
): 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 = {};
|
let savedConfigDefaults: CodexConfiguredDefaults = {};
|
||||||
try {
|
try {
|
||||||
const response = toObjectRecord(await client.request("getUserSavedConfig", {}));
|
const response = toObjectRecord(await client.request("getUserSavedConfig", {}));
|
||||||
@@ -3119,26 +3141,7 @@ async function readCodexConfiguredDefaults(
|
|||||||
logger.debug({ error }, "Failed to read Codex saved config defaults");
|
logger.debug({ error }, "Failed to read Codex saved config defaults");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (savedConfigDefaults.model && savedConfigDefaults.thinkingOptionId) {
|
return mergeCodexConfiguredDefaults(configReadDefaults, savedConfigDefaults);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CodexSkillPromptBlock {
|
interface CodexSkillPromptBlock {
|
||||||
@@ -5119,7 +5122,11 @@ export class CodexAppServerAgentSession implements AgentSession {
|
|||||||
let model = this.config.model;
|
let model = this.config.model;
|
||||||
let thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId);
|
let thinkingOptionId = normalizeCodexThinkingOptionId(this.config.thinkingOptionId);
|
||||||
if (!model || !thinkingOptionId) {
|
if (!model || !thinkingOptionId) {
|
||||||
configuredDefaults = await readCodexConfiguredDefaults(this.client, this.logger);
|
configuredDefaults = await readCodexConfiguredDefaults(
|
||||||
|
this.client,
|
||||||
|
this.logger,
|
||||||
|
this.config.cwd ?? null,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!model) {
|
if (!model) {
|
||||||
model = configuredDefaults.model;
|
model = configuredDefaults.model;
|
||||||
@@ -7323,7 +7330,7 @@ export class CodexAppServerAgentClient implements AgentClient {
|
|||||||
const parsedResponse = CodexModelListResponseSchema.safeParse(rawResponse);
|
const parsedResponse = CodexModelListResponseSchema.safeParse(rawResponse);
|
||||||
const models = parsedResponse.success ? (parsedResponse.data.data ?? []) : [];
|
const models = parsedResponse.success ? (parsedResponse.data.data ?? []) : [];
|
||||||
const configuredDefaults = await runProviderRefreshActivity(context, "config/read", () =>
|
const configuredDefaults = await runProviderRefreshActivity(context, "config/read", () =>
|
||||||
readCodexConfiguredDefaults(client!, this.logger),
|
readCodexConfiguredDefaults(client!, this.logger, null),
|
||||||
);
|
);
|
||||||
const configuredDefaultModelId = configuredDefaults.model;
|
const configuredDefaultModelId = configuredDefaults.model;
|
||||||
const configuredDefaultThinkingOptionId = configuredDefaults.thinkingOptionId;
|
const configuredDefaultThinkingOptionId = configuredDefaults.thinkingOptionId;
|
||||||
|
|||||||
@@ -355,7 +355,8 @@ test.each([
|
|||||||
await resumed?.close();
|
await resumed?.close();
|
||||||
await original?.close();
|
await original?.close();
|
||||||
await client.shutdown();
|
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,
|
240_000,
|
||||||
|
|||||||
@@ -221,6 +221,15 @@ function deriveOpencodeTaskDetail(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toOpencodeEditInput(value: { filePath: string; oldString?: string; newString?: string }) {
|
||||||
|
return {
|
||||||
|
filePath: value.filePath,
|
||||||
|
oldString: nonEmptyString(value.oldString),
|
||||||
|
newString: nonEmptyString(value.newString),
|
||||||
|
unifiedDiff: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const OpencodeEditInputSchema = z.union([
|
const OpencodeEditInputSchema = z.union([
|
||||||
z
|
z
|
||||||
.object({
|
.object({
|
||||||
@@ -229,12 +238,12 @@ const OpencodeEditInputSchema = z.union([
|
|||||||
newString: z.string().optional(),
|
newString: z.string().optional(),
|
||||||
})
|
})
|
||||||
.passthrough()
|
.passthrough()
|
||||||
.transform((value) => ({
|
.transform(toOpencodeEditInput),
|
||||||
filePath: value.filePath,
|
// OpenCode v2's edit tool names the file `path`.
|
||||||
oldString: nonEmptyString(value.oldString),
|
z
|
||||||
newString: nonEmptyString(value.newString),
|
.object({ path: z.string(), oldString: z.string(), newString: z.string() })
|
||||||
unifiedDiff: undefined,
|
.passthrough()
|
||||||
})),
|
.transform((value) => toOpencodeEditInput({ ...value, filePath: value.path })),
|
||||||
ToolEditInputSchema,
|
ToolEditInputSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type { SessionMessageAssistant } from "@opencode/client";
|
||||||
|
import { expect, test } from "vitest";
|
||||||
|
import { V2Timeline } from "./timeline.js";
|
||||||
|
|
||||||
|
function assistant(content: SessionMessageAssistant["content"]): SessionMessageAssistant {
|
||||||
|
return {
|
||||||
|
id: "answer",
|
||||||
|
type: "assistant",
|
||||||
|
agent: "build",
|
||||||
|
model: { providerID: "test", id: "model" },
|
||||||
|
time: { created: 2 },
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test("a completed edit carries the replaced and replacement text", () => {
|
||||||
|
// Captured from OpenCode 2.0.18, whose edit tool takes `path` instead of `filePath`.
|
||||||
|
const edit = assistant([
|
||||||
|
{
|
||||||
|
type: "tool",
|
||||||
|
id: "call-edit",
|
||||||
|
name: "edit",
|
||||||
|
executed: false,
|
||||||
|
state: {
|
||||||
|
status: "completed",
|
||||||
|
input: { path: "a.txt", oldString: "beta", newString: "BETA" },
|
||||||
|
content: [{ type: "text", text: "Edited a.txt (1 replacement)" }],
|
||||||
|
metadata: {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
file: "a.txt",
|
||||||
|
patch:
|
||||||
|
"Index: a.txt\n===================================================================\n--- a.txt\n+++ a.txt\n@@ -1,3 +1,3 @@\n alpha\n-beta\n+BETA\n gamma\n",
|
||||||
|
status: "modified",
|
||||||
|
additions: 1,
|
||||||
|
deletions: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
truncated: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
time: { created: 2, ran: 2, completed: 2 },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(new V2Timeline().messages([edit])).toMatchObject([
|
||||||
|
{
|
||||||
|
item: {
|
||||||
|
type: "tool_call",
|
||||||
|
name: "edit",
|
||||||
|
status: "completed",
|
||||||
|
detail: { type: "edit", filePath: "a.txt", oldString: "beta", newString: "BETA" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user