From 0d9a69d00fcd26288b3c61c3ded985673b21b71d Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 7 Jan 2026 14:42:16 +0700 Subject: [PATCH] fix: branch reset on workdir change and Codex MCP server config merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reset git branch to current when user changes working directory in create-agent modal - Fix Codex MCP servers not being passed from extra.codex.mcp_servers config - Remove macbook host from default daemons - Update test expectations and structure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../app/src/components/create-agent-modal.tsx | 1 + packages/app/src/components/git-diff-pane.tsx | 6 +-- .../src/contexts/daemon-registry-context.tsx | 1 - .../src/server/agent/mcp-server.test.ts | 3 ++ .../agent/providers/claude-agent.test.ts | 6 +-- .../agent/providers/codex-mcp-agent.test.ts | 50 +++++++++---------- .../server/agent/providers/codex-mcp-agent.ts | 16 ++++-- packages/server/src/server/daemon.e2e.test.ts | 14 +++--- 8 files changed, 52 insertions(+), 45 deletions(-) diff --git a/packages/app/src/components/create-agent-modal.tsx b/packages/app/src/components/create-agent-modal.tsx index ec79058e2..f54e31992 100644 --- a/packages/app/src/components/create-agent-modal.tsx +++ b/packages/app/src/components/create-agent-modal.tsx @@ -436,6 +436,7 @@ function AgentFlowModal({ (value: string) => { setWorkingDirFromUser(value); setErrorMessage(""); + shouldSyncBaseBranchRef.current = true; }, [setWorkingDirFromUser] ); diff --git a/packages/app/src/components/git-diff-pane.tsx b/packages/app/src/components/git-diff-pane.tsx index 8ff19b2da..46ab4293a 100644 --- a/packages/app/src/components/git-diff-pane.tsx +++ b/packages/app/src/components/git-diff-pane.tsx @@ -119,6 +119,7 @@ function DiffLineView({ line }: { line: DiffLine }) { function DiffFileSection({ file, defaultExpanded = true, testID }: DiffFileSectionProps) { const { theme } = useUnistyles(); const [isExpanded, setIsExpanded] = useState(defaultExpanded); + const [scrollViewWidth, setScrollViewWidth] = useState(0); const horizontalScroll = useHorizontalScrollOptional(); const scrollId = useId(); @@ -190,8 +191,9 @@ function DiffFileSection({ file, defaultExpanded = true, testID }: DiffFileSecti contentContainerStyle={styles.diffContentInner} onScroll={handleScroll} scrollEventThrottle={16} + onLayout={(e) => setScrollViewWidth(e.nativeEvent.layout.width)} > - + 0 && { minWidth: scrollViewWidth }]}> {file.hunks.map((hunk, hunkIndex) => hunk.lines.map((line, lineIndex) => ( @@ -388,8 +390,6 @@ const styles = StyleSheet.create((theme) => ({ flexDirection: "column", }, linesContainer: { - alignSelf: "flex-start", - minWidth: "100%", backgroundColor: "#0d1117", }, diffLineContainer: { diff --git a/packages/app/src/contexts/daemon-registry-context.tsx b/packages/app/src/contexts/daemon-registry-context.tsx index c94d50200..e3724dd0e 100644 --- a/packages/app/src/contexts/daemon-registry-context.tsx +++ b/packages/app/src/contexts/daemon-registry-context.tsx @@ -8,7 +8,6 @@ const LEGACY_SETTINGS_KEY = "@paseo:settings"; const FALLBACK_DAEMON_URL = "ws://localhost:6767/ws"; const DEFAULT_DAEMONS: Array<{ label: string; wsUrl: string; restUrl?: string | null }> = [ { label: "localhost", wsUrl: "ws://localhost:6767/ws" }, - { label: "macbook", wsUrl: "ws://mohameds-macbook-pro:6767/ws" }, ]; const DAEMON_REGISTRY_QUERY_KEY = ["daemon-registry"]; diff --git a/packages/server/src/server/agent/mcp-server.test.ts b/packages/server/src/server/agent/mcp-server.test.ts index db64adec8..5f49388d6 100644 --- a/packages/server/src/server/agent/mcp-server.test.ts +++ b/packages/server/src/server/agent/mcp-server.test.ts @@ -53,12 +53,14 @@ describe("create_agent MCP tool", () => { const missingTitle = await tool.inputSchema.safeParseAsync({ cwd: "/tmp/repo", + initialMode: "default", }); expect(missingTitle.success).toBe(false); expect(missingTitle.error.issues[0].path).toEqual(["title"]); const tooLong = await tool.inputSchema.safeParseAsync({ cwd: "/tmp/repo", + initialMode: "default", title: "x".repeat(41), }); expect(tooLong.success).toBe(false); @@ -66,6 +68,7 @@ describe("create_agent MCP tool", () => { const ok = await tool.inputSchema.safeParseAsync({ cwd: "/tmp/repo", + initialMode: "default", title: "Short title", }); expect(ok.success).toBe(true); diff --git a/packages/server/src/server/agent/providers/claude-agent.test.ts b/packages/server/src/server/agent/providers/claude-agent.test.ts index d289eb3ac..efe545a32 100644 --- a/packages/server/src/server/agent/providers/claude-agent.test.ts +++ b/packages/server/src/server/agent/providers/claude-agent.test.ts @@ -509,7 +509,7 @@ describe("ClaudeAgentClient (SDK integration)", () => { timeline.some( (item) => item.type === "tool_call" && - item.server === "permission" && + item.name === "permission" && item.status === "granted" ) ).toBe(true); @@ -583,7 +583,7 @@ describe("ClaudeAgentClient (SDK integration)", () => { timeline.some( (item) => item.type === "tool_call" && - item.server === "permission" && + item.name === "permission" && item.status === "denied" ) ).toBe(true); @@ -662,7 +662,7 @@ describe("ClaudeAgentClient (SDK integration)", () => { timeline.some( (item) => item.type === "tool_call" && - item.server === "permission" && + item.name === "permission" && item.status === "denied" ) ).toBe(true); diff --git a/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts b/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts index 9f2699136..ab4de7060 100644 --- a/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts @@ -530,7 +530,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => { if (event.item.type === "reasoning") { sawReasoning = true; } - if (event.item.type === "tool_call" && event.item.server !== "permission") { + if (event.item.type === "tool_call" && event.item.name !== "permission") { toolCalls.push(event.item); } } @@ -569,7 +569,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => { .reverse() .find( (item) => - item.server === "command" && item.status !== "running" + item.name === "shell" && item.status !== "running" ); expect(commandToolCall).toBeTruthy(); @@ -665,7 +665,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => { if ( timelineItems.some( - (item) => item.type === "tool_call" && item.server === "file_change" + (item) => item.type === "tool_call" && item.name === "apply_patch" ) ) { rawItemTypes.add("file_change"); @@ -674,8 +674,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => { timelineItems.some( (item) => item.type === "tool_call" && - item.server === "test" && - item.tool === "echo" + item.name === "test.echo" ) ) { rawItemTypes.add("mcp_tool_call"); @@ -684,8 +683,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => { timelineItems.some( (item) => item.type === "tool_call" && - item.server === "web_search" && - item.tool === "web_search" + item.name === "web_search" ) ) { rawItemTypes.add("web_search"); @@ -707,23 +705,21 @@ describe("CodexMcpAgentClient (MCP integration)", () => { expect( timelineItems.some( - (item) => item.type === "tool_call" && item.server === "file_change" + (item) => item.type === "tool_call" && item.name === "apply_patch" ) ).toBe(true); expect( timelineItems.some( (item) => item.type === "tool_call" && - item.server === "test" && - item.tool === "echo" + item.name === "test.echo" ) ).toBe(true); expect( timelineItems.some( (item) => item.type === "tool_call" && - item.server === "web_search" && - item.tool === "web_search" + item.name === "web_search" ) ).toBe(true); expect( @@ -807,7 +803,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => { permissionResolved = true; } if (event.type === "timeline" && providerFromEvent(event) === "codex") { - if (event.item.type === "tool_call" && event.item.server !== "permission") { + if (event.item.type === "tool_call" && event.item.name !== "permission") { toolCalls.push(event.item); } } @@ -820,7 +816,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => { expect.soft(permissionResolved).toBe(true); const commandCalls = toolCalls.filter( - (item) => item.server === "command" && item.status === "completed" + (item) => item.name === "shell" && item.status === "completed" ); expect.soft(commandCalls.length).toBeGreaterThanOrEqual(2); @@ -842,7 +838,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => { expect.soft(extractExitCode(stderrCall?.output)).toBe(0); const fileChangeCalls = toolCalls.filter( - (item) => item.server === "file_change" && item.tool === "apply_patch" + (item) => item.name === "apply_patch" ); expect.soft(fileChangeCalls.length).toBeGreaterThanOrEqual(2); expect.soft( @@ -859,21 +855,21 @@ describe("CodexMcpAgentClient (MCP integration)", () => { ).toBe(true); const readCall = toolCalls.find( - (item) => item.tool === "read_file" && item.status === "completed" + (item) => item.name === "read_file" && item.status === "completed" ); expect.soft(readCall).toBeTruthy(); expect.soft(stringifyUnknown(readCall?.input)).toContain("tool-create.txt"); expect.soft(stringifyUnknown(readCall?.output)).toContain("beta"); const mcpCall = toolCalls.find( - (item) => item.server === "test" && item.tool === "echo" + (item) => item.name === "test.echo" ); expect.soft(mcpCall).toBeTruthy(); expect.soft(stringifyUnknown(mcpCall?.input)).toContain("mcp-ok"); expect.soft(stringifyUnknown(mcpCall?.output)).toContain("mcp-ok"); const webSearchCall = toolCalls.find( - (item) => item.server === "web_search" && item.tool === "web_search" + (item) => item.name === "web_search" ); expect.soft(webSearchCall).toBeTruthy(); expect.soft(stringifyUnknown(webSearchCall?.input)).toContain("OpenAI Codex MCP"); @@ -894,11 +890,11 @@ describe("CodexMcpAgentClient (MCP integration)", () => { } } const commandCallIds = toolCalls - .filter((item) => item.server === "command") + .filter((item) => item.name === "shell") .map((item) => item.callId) .filter((callId): callId is string => typeof callId === "string"); const fileChangeCallIds = toolCalls - .filter((item) => item.server === "file_change") + .filter((item) => item.name === "apply_patch") .map((item) => item.callId) .filter((callId): callId is string => typeof callId === "string"); @@ -1147,13 +1143,13 @@ describe("CodexMcpAgentClient (MCP integration)", () => { timelineItems.some( (item) => item.type === "tool_call" && - item.server === "permission" && + item.name === "permission" && item.status === "granted" ) ).toBe(true); expect( timelineItems.some( - (item) => item.type === "tool_call" && item.server === "command" + (item) => item.type === "tool_call" && item.name === "shell" ) ).toBe(true); expect(existsSync(filePath)).toBe(true); @@ -1211,7 +1207,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => { timelineItems.some( (item) => item.type === "tool_call" && - item.server === "permission" && + item.name === "permission" && item.status === "requested" ) ).toBe(true); @@ -1282,7 +1278,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => { timelineItems.some( (item) => item.type === "tool_call" && - item.server === "permission" && + item.name === "permission" && item.status === "denied" ) ).toBe(true); @@ -1363,7 +1359,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => { timelineItems.some( (item) => item.type === "tool_call" && - item.server === "permission" && + item.name === "permission" && item.status === "denied" ) ).toBe(true); @@ -1419,7 +1415,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => { event.type === "timeline" && providerFromEvent(event) === "codex" && event.item.type === "tool_call" && - event.item.server === "command" && + event.item.name === "shell" && isSleepCommandToolCall(event.item) ) { sawSleepCommand = true; @@ -1494,7 +1490,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => { event.type === "timeline" && providerFromEvent(event) === "codex" && event.item.type === "tool_call" && - event.item.server === "command" + event.item.name === "shell" ) { const commandText = commandTextFromInput(event.item.input); if (commandText && commandText.includes(marker)) { diff --git a/packages/server/src/server/agent/providers/codex-mcp-agent.ts b/packages/server/src/server/agent/providers/codex-mcp-agent.ts index 5a4b55a24..a8315dd07 100644 --- a/packages/server/src/server/agent/providers/codex-mcp-agent.ts +++ b/packages/server/src/server/agent/providers/codex-mcp-agent.ts @@ -2523,7 +2523,17 @@ function buildCodexMcpConfig( args: ["@playwright/mcp", "--headless", "--isolated"], }; - // Merge user-provided MCP servers (they take precedence) + // Merge MCP servers from extra.codex.mcp_servers (legacy location) + const extraCodex = config.extra?.codex as Record | undefined; + if (extraCodex?.mcp_servers && typeof extraCodex.mcp_servers === "object") { + for (const [name, serverConfig] of Object.entries(extraCodex.mcp_servers as Record)) { + if (typeof serverConfig === "object" && serverConfig !== null) { + mcpServers[name] = serverConfig as CodexMcpServerConfig; + } + } + } + + // Merge user-provided MCP servers (they take highest precedence) if (config.mcpServers) { for (const [name, serverConfig] of Object.entries(config.mcpServers)) { if (typeof serverConfig === "object" && serverConfig !== null) { @@ -3098,7 +3108,7 @@ class CodexMcpAgentSession implements AgentSession { type: "timeline", provider: CODEX_PROVIDER, item: createToolCallTimelineItem({ - name: pending.request.name, + name: "permission", status, callId: pending.request.id, input: pending.request.input, @@ -3405,7 +3415,7 @@ class CodexMcpAgentSession implements AgentSession { type: "timeline", provider: CODEX_PROVIDER, item: createToolCallTimelineItem({ - name: request.name, + name: "permission", status: "requested", callId: request.id, input: request.input, diff --git a/packages/server/src/server/daemon.e2e.test.ts b/packages/server/src/server/daemon.e2e.test.ts index 54d617bdd..a53b0d1ff 100644 --- a/packages/server/src/server/daemon.e2e.test.ts +++ b/packages/server/src/server/daemon.e2e.test.ts @@ -172,7 +172,7 @@ describe("daemon E2E", () => { const hasGranted = timelineItems.some( (item) => item.type === "tool_call" && - item.server === "permission" && + item.name === "permission" && item.status === "granted" ); expect(hasGranted).toBe(true); @@ -256,7 +256,7 @@ describe("daemon E2E", () => { const hasDenied = timelineItems.some( (item) => item.type === "tool_call" && - item.server === "permission" && + item.name === "permission" && item.status === "denied" ); expect(hasDenied).toBe(true); @@ -799,8 +799,7 @@ describe("daemon E2E", () => { const hasCreateAgentCall = timelineItems.some( (item) => item.type === "tool_call" && - item.tool === "create_agent" && - item.server === "agent-control" + item.name === "agent-control.create_agent" ); expect(hasCreateAgentCall).toBe(true); @@ -827,8 +826,7 @@ describe("daemon E2E", () => { const createAgentCall = timelineItems.find( (item) => item.type === "tool_call" && - item.tool === "create_agent" && - item.server === "agent-control" + item.name === "agent-control.create_agent" ); let childAgentId: string | null = null; @@ -1372,7 +1370,7 @@ describe("daemon E2E", () => { const hasGranted = timelineItems.some( (item) => item.type === "tool_call" && - item.server === "permission" && + item.name === "permission" && item.status === "granted" ); expect(hasGranted).toBe(true); @@ -1463,7 +1461,7 @@ describe("daemon E2E", () => { const hasDenied = timelineItems.some( (item) => item.type === "tool_call" && - item.server === "permission" && + item.name === "permission" && item.status === "denied" ); expect(hasDenied).toBe(true);