fix: branch reset on workdir change and Codex MCP server config merge

- 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 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2026-01-07 14:42:16 +07:00
co-authored by Claude Opus 4.5
parent 66881511b6
commit 0d9a69d00f
8 changed files with 52 additions and 45 deletions
@@ -436,6 +436,7 @@ function AgentFlowModal({
(value: string) => { (value: string) => {
setWorkingDirFromUser(value); setWorkingDirFromUser(value);
setErrorMessage(""); setErrorMessage("");
shouldSyncBaseBranchRef.current = true;
}, },
[setWorkingDirFromUser] [setWorkingDirFromUser]
); );
@@ -119,6 +119,7 @@ function DiffLineView({ line }: { line: DiffLine }) {
function DiffFileSection({ file, defaultExpanded = true, testID }: DiffFileSectionProps) { function DiffFileSection({ file, defaultExpanded = true, testID }: DiffFileSectionProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const [isExpanded, setIsExpanded] = useState(defaultExpanded); const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const [scrollViewWidth, setScrollViewWidth] = useState(0);
const horizontalScroll = useHorizontalScrollOptional(); const horizontalScroll = useHorizontalScrollOptional();
const scrollId = useId(); const scrollId = useId();
@@ -190,8 +191,9 @@ function DiffFileSection({ file, defaultExpanded = true, testID }: DiffFileSecti
contentContainerStyle={styles.diffContentInner} contentContainerStyle={styles.diffContentInner}
onScroll={handleScroll} onScroll={handleScroll}
scrollEventThrottle={16} scrollEventThrottle={16}
onLayout={(e) => setScrollViewWidth(e.nativeEvent.layout.width)}
> >
<View style={styles.linesContainer}> <View style={[styles.linesContainer, scrollViewWidth > 0 && { minWidth: scrollViewWidth }]}>
{file.hunks.map((hunk, hunkIndex) => {file.hunks.map((hunk, hunkIndex) =>
hunk.lines.map((line, lineIndex) => ( hunk.lines.map((line, lineIndex) => (
<DiffLineView key={`${hunkIndex}-${lineIndex}`} line={line} /> <DiffLineView key={`${hunkIndex}-${lineIndex}`} line={line} />
@@ -388,8 +390,6 @@ const styles = StyleSheet.create((theme) => ({
flexDirection: "column", flexDirection: "column",
}, },
linesContainer: { linesContainer: {
alignSelf: "flex-start",
minWidth: "100%",
backgroundColor: "#0d1117", backgroundColor: "#0d1117",
}, },
diffLineContainer: { diffLineContainer: {
@@ -8,7 +8,6 @@ const LEGACY_SETTINGS_KEY = "@paseo:settings";
const FALLBACK_DAEMON_URL = "ws://localhost:6767/ws"; const FALLBACK_DAEMON_URL = "ws://localhost:6767/ws";
const DEFAULT_DAEMONS: Array<{ label: string; wsUrl: string; restUrl?: string | null }> = [ const DEFAULT_DAEMONS: Array<{ label: string; wsUrl: string; restUrl?: string | null }> = [
{ label: "localhost", wsUrl: "ws://localhost:6767/ws" }, { label: "localhost", wsUrl: "ws://localhost:6767/ws" },
{ label: "macbook", wsUrl: "ws://mohameds-macbook-pro:6767/ws" },
]; ];
const DAEMON_REGISTRY_QUERY_KEY = ["daemon-registry"]; const DAEMON_REGISTRY_QUERY_KEY = ["daemon-registry"];
@@ -53,12 +53,14 @@ describe("create_agent MCP tool", () => {
const missingTitle = await tool.inputSchema.safeParseAsync({ const missingTitle = await tool.inputSchema.safeParseAsync({
cwd: "/tmp/repo", cwd: "/tmp/repo",
initialMode: "default",
}); });
expect(missingTitle.success).toBe(false); expect(missingTitle.success).toBe(false);
expect(missingTitle.error.issues[0].path).toEqual(["title"]); expect(missingTitle.error.issues[0].path).toEqual(["title"]);
const tooLong = await tool.inputSchema.safeParseAsync({ const tooLong = await tool.inputSchema.safeParseAsync({
cwd: "/tmp/repo", cwd: "/tmp/repo",
initialMode: "default",
title: "x".repeat(41), title: "x".repeat(41),
}); });
expect(tooLong.success).toBe(false); expect(tooLong.success).toBe(false);
@@ -66,6 +68,7 @@ describe("create_agent MCP tool", () => {
const ok = await tool.inputSchema.safeParseAsync({ const ok = await tool.inputSchema.safeParseAsync({
cwd: "/tmp/repo", cwd: "/tmp/repo",
initialMode: "default",
title: "Short title", title: "Short title",
}); });
expect(ok.success).toBe(true); expect(ok.success).toBe(true);
@@ -509,7 +509,7 @@ describe("ClaudeAgentClient (SDK integration)", () => {
timeline.some( timeline.some(
(item) => (item) =>
item.type === "tool_call" && item.type === "tool_call" &&
item.server === "permission" && item.name === "permission" &&
item.status === "granted" item.status === "granted"
) )
).toBe(true); ).toBe(true);
@@ -583,7 +583,7 @@ describe("ClaudeAgentClient (SDK integration)", () => {
timeline.some( timeline.some(
(item) => (item) =>
item.type === "tool_call" && item.type === "tool_call" &&
item.server === "permission" && item.name === "permission" &&
item.status === "denied" item.status === "denied"
) )
).toBe(true); ).toBe(true);
@@ -662,7 +662,7 @@ describe("ClaudeAgentClient (SDK integration)", () => {
timeline.some( timeline.some(
(item) => (item) =>
item.type === "tool_call" && item.type === "tool_call" &&
item.server === "permission" && item.name === "permission" &&
item.status === "denied" item.status === "denied"
) )
).toBe(true); ).toBe(true);
@@ -530,7 +530,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
if (event.item.type === "reasoning") { if (event.item.type === "reasoning") {
sawReasoning = true; 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); toolCalls.push(event.item);
} }
} }
@@ -569,7 +569,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
.reverse() .reverse()
.find( .find(
(item) => (item) =>
item.server === "command" && item.status !== "running" item.name === "shell" && item.status !== "running"
); );
expect(commandToolCall).toBeTruthy(); expect(commandToolCall).toBeTruthy();
@@ -665,7 +665,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
if ( if (
timelineItems.some( timelineItems.some(
(item) => item.type === "tool_call" && item.server === "file_change" (item) => item.type === "tool_call" && item.name === "apply_patch"
) )
) { ) {
rawItemTypes.add("file_change"); rawItemTypes.add("file_change");
@@ -674,8 +674,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
timelineItems.some( timelineItems.some(
(item) => (item) =>
item.type === "tool_call" && item.type === "tool_call" &&
item.server === "test" && item.name === "test.echo"
item.tool === "echo"
) )
) { ) {
rawItemTypes.add("mcp_tool_call"); rawItemTypes.add("mcp_tool_call");
@@ -684,8 +683,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
timelineItems.some( timelineItems.some(
(item) => (item) =>
item.type === "tool_call" && item.type === "tool_call" &&
item.server === "web_search" && item.name === "web_search"
item.tool === "web_search"
) )
) { ) {
rawItemTypes.add("web_search"); rawItemTypes.add("web_search");
@@ -707,23 +705,21 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
expect( expect(
timelineItems.some( timelineItems.some(
(item) => item.type === "tool_call" && item.server === "file_change" (item) => item.type === "tool_call" && item.name === "apply_patch"
) )
).toBe(true); ).toBe(true);
expect( expect(
timelineItems.some( timelineItems.some(
(item) => (item) =>
item.type === "tool_call" && item.type === "tool_call" &&
item.server === "test" && item.name === "test.echo"
item.tool === "echo"
) )
).toBe(true); ).toBe(true);
expect( expect(
timelineItems.some( timelineItems.some(
(item) => (item) =>
item.type === "tool_call" && item.type === "tool_call" &&
item.server === "web_search" && item.name === "web_search"
item.tool === "web_search"
) )
).toBe(true); ).toBe(true);
expect( expect(
@@ -807,7 +803,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
permissionResolved = true; permissionResolved = true;
} }
if (event.type === "timeline" && providerFromEvent(event) === "codex") { 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); toolCalls.push(event.item);
} }
} }
@@ -820,7 +816,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
expect.soft(permissionResolved).toBe(true); expect.soft(permissionResolved).toBe(true);
const commandCalls = toolCalls.filter( const commandCalls = toolCalls.filter(
(item) => item.server === "command" && item.status === "completed" (item) => item.name === "shell" && item.status === "completed"
); );
expect.soft(commandCalls.length).toBeGreaterThanOrEqual(2); expect.soft(commandCalls.length).toBeGreaterThanOrEqual(2);
@@ -842,7 +838,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
expect.soft(extractExitCode(stderrCall?.output)).toBe(0); expect.soft(extractExitCode(stderrCall?.output)).toBe(0);
const fileChangeCalls = toolCalls.filter( 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(fileChangeCalls.length).toBeGreaterThanOrEqual(2);
expect.soft( expect.soft(
@@ -859,21 +855,21 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
).toBe(true); ).toBe(true);
const readCall = toolCalls.find( 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(readCall).toBeTruthy();
expect.soft(stringifyUnknown(readCall?.input)).toContain("tool-create.txt"); expect.soft(stringifyUnknown(readCall?.input)).toContain("tool-create.txt");
expect.soft(stringifyUnknown(readCall?.output)).toContain("beta"); expect.soft(stringifyUnknown(readCall?.output)).toContain("beta");
const mcpCall = toolCalls.find( const mcpCall = toolCalls.find(
(item) => item.server === "test" && item.tool === "echo" (item) => item.name === "test.echo"
); );
expect.soft(mcpCall).toBeTruthy(); expect.soft(mcpCall).toBeTruthy();
expect.soft(stringifyUnknown(mcpCall?.input)).toContain("mcp-ok"); expect.soft(stringifyUnknown(mcpCall?.input)).toContain("mcp-ok");
expect.soft(stringifyUnknown(mcpCall?.output)).toContain("mcp-ok"); expect.soft(stringifyUnknown(mcpCall?.output)).toContain("mcp-ok");
const webSearchCall = toolCalls.find( const webSearchCall = toolCalls.find(
(item) => item.server === "web_search" && item.tool === "web_search" (item) => item.name === "web_search"
); );
expect.soft(webSearchCall).toBeTruthy(); expect.soft(webSearchCall).toBeTruthy();
expect.soft(stringifyUnknown(webSearchCall?.input)).toContain("OpenAI Codex MCP"); expect.soft(stringifyUnknown(webSearchCall?.input)).toContain("OpenAI Codex MCP");
@@ -894,11 +890,11 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
} }
} }
const commandCallIds = toolCalls const commandCallIds = toolCalls
.filter((item) => item.server === "command") .filter((item) => item.name === "shell")
.map((item) => item.callId) .map((item) => item.callId)
.filter((callId): callId is string => typeof callId === "string"); .filter((callId): callId is string => typeof callId === "string");
const fileChangeCallIds = toolCalls const fileChangeCallIds = toolCalls
.filter((item) => item.server === "file_change") .filter((item) => item.name === "apply_patch")
.map((item) => item.callId) .map((item) => item.callId)
.filter((callId): callId is string => typeof callId === "string"); .filter((callId): callId is string => typeof callId === "string");
@@ -1147,13 +1143,13 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
timelineItems.some( timelineItems.some(
(item) => (item) =>
item.type === "tool_call" && item.type === "tool_call" &&
item.server === "permission" && item.name === "permission" &&
item.status === "granted" item.status === "granted"
) )
).toBe(true); ).toBe(true);
expect( expect(
timelineItems.some( timelineItems.some(
(item) => item.type === "tool_call" && item.server === "command" (item) => item.type === "tool_call" && item.name === "shell"
) )
).toBe(true); ).toBe(true);
expect(existsSync(filePath)).toBe(true); expect(existsSync(filePath)).toBe(true);
@@ -1211,7 +1207,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
timelineItems.some( timelineItems.some(
(item) => (item) =>
item.type === "tool_call" && item.type === "tool_call" &&
item.server === "permission" && item.name === "permission" &&
item.status === "requested" item.status === "requested"
) )
).toBe(true); ).toBe(true);
@@ -1282,7 +1278,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
timelineItems.some( timelineItems.some(
(item) => (item) =>
item.type === "tool_call" && item.type === "tool_call" &&
item.server === "permission" && item.name === "permission" &&
item.status === "denied" item.status === "denied"
) )
).toBe(true); ).toBe(true);
@@ -1363,7 +1359,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
timelineItems.some( timelineItems.some(
(item) => (item) =>
item.type === "tool_call" && item.type === "tool_call" &&
item.server === "permission" && item.name === "permission" &&
item.status === "denied" item.status === "denied"
) )
).toBe(true); ).toBe(true);
@@ -1419,7 +1415,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
event.type === "timeline" && event.type === "timeline" &&
providerFromEvent(event) === "codex" && providerFromEvent(event) === "codex" &&
event.item.type === "tool_call" && event.item.type === "tool_call" &&
event.item.server === "command" && event.item.name === "shell" &&
isSleepCommandToolCall(event.item) isSleepCommandToolCall(event.item)
) { ) {
sawSleepCommand = true; sawSleepCommand = true;
@@ -1494,7 +1490,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
event.type === "timeline" && event.type === "timeline" &&
providerFromEvent(event) === "codex" && providerFromEvent(event) === "codex" &&
event.item.type === "tool_call" && event.item.type === "tool_call" &&
event.item.server === "command" event.item.name === "shell"
) { ) {
const commandText = commandTextFromInput(event.item.input); const commandText = commandTextFromInput(event.item.input);
if (commandText && commandText.includes(marker)) { if (commandText && commandText.includes(marker)) {
@@ -2523,7 +2523,17 @@ function buildCodexMcpConfig(
args: ["@playwright/mcp", "--headless", "--isolated"], 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<string, unknown> | undefined;
if (extraCodex?.mcp_servers && typeof extraCodex.mcp_servers === "object") {
for (const [name, serverConfig] of Object.entries(extraCodex.mcp_servers as Record<string, unknown>)) {
if (typeof serverConfig === "object" && serverConfig !== null) {
mcpServers[name] = serverConfig as CodexMcpServerConfig;
}
}
}
// Merge user-provided MCP servers (they take highest precedence)
if (config.mcpServers) { if (config.mcpServers) {
for (const [name, serverConfig] of Object.entries(config.mcpServers)) { for (const [name, serverConfig] of Object.entries(config.mcpServers)) {
if (typeof serverConfig === "object" && serverConfig !== null) { if (typeof serverConfig === "object" && serverConfig !== null) {
@@ -3098,7 +3108,7 @@ class CodexMcpAgentSession implements AgentSession {
type: "timeline", type: "timeline",
provider: CODEX_PROVIDER, provider: CODEX_PROVIDER,
item: createToolCallTimelineItem({ item: createToolCallTimelineItem({
name: pending.request.name, name: "permission",
status, status,
callId: pending.request.id, callId: pending.request.id,
input: pending.request.input, input: pending.request.input,
@@ -3405,7 +3415,7 @@ class CodexMcpAgentSession implements AgentSession {
type: "timeline", type: "timeline",
provider: CODEX_PROVIDER, provider: CODEX_PROVIDER,
item: createToolCallTimelineItem({ item: createToolCallTimelineItem({
name: request.name, name: "permission",
status: "requested", status: "requested",
callId: request.id, callId: request.id,
input: request.input, input: request.input,
@@ -172,7 +172,7 @@ describe("daemon E2E", () => {
const hasGranted = timelineItems.some( const hasGranted = timelineItems.some(
(item) => (item) =>
item.type === "tool_call" && item.type === "tool_call" &&
item.server === "permission" && item.name === "permission" &&
item.status === "granted" item.status === "granted"
); );
expect(hasGranted).toBe(true); expect(hasGranted).toBe(true);
@@ -256,7 +256,7 @@ describe("daemon E2E", () => {
const hasDenied = timelineItems.some( const hasDenied = timelineItems.some(
(item) => (item) =>
item.type === "tool_call" && item.type === "tool_call" &&
item.server === "permission" && item.name === "permission" &&
item.status === "denied" item.status === "denied"
); );
expect(hasDenied).toBe(true); expect(hasDenied).toBe(true);
@@ -799,8 +799,7 @@ describe("daemon E2E", () => {
const hasCreateAgentCall = timelineItems.some( const hasCreateAgentCall = timelineItems.some(
(item) => (item) =>
item.type === "tool_call" && item.type === "tool_call" &&
item.tool === "create_agent" && item.name === "agent-control.create_agent"
item.server === "agent-control"
); );
expect(hasCreateAgentCall).toBe(true); expect(hasCreateAgentCall).toBe(true);
@@ -827,8 +826,7 @@ describe("daemon E2E", () => {
const createAgentCall = timelineItems.find( const createAgentCall = timelineItems.find(
(item) => (item) =>
item.type === "tool_call" && item.type === "tool_call" &&
item.tool === "create_agent" && item.name === "agent-control.create_agent"
item.server === "agent-control"
); );
let childAgentId: string | null = null; let childAgentId: string | null = null;
@@ -1372,7 +1370,7 @@ describe("daemon E2E", () => {
const hasGranted = timelineItems.some( const hasGranted = timelineItems.some(
(item) => (item) =>
item.type === "tool_call" && item.type === "tool_call" &&
item.server === "permission" && item.name === "permission" &&
item.status === "granted" item.status === "granted"
); );
expect(hasGranted).toBe(true); expect(hasGranted).toBe(true);
@@ -1463,7 +1461,7 @@ describe("daemon E2E", () => {
const hasDenied = timelineItems.some( const hasDenied = timelineItems.some(
(item) => (item) =>
item.type === "tool_call" && item.type === "tool_call" &&
item.server === "permission" && item.name === "permission" &&
item.status === "denied" item.status === "denied"
); );
expect(hasDenied).toBe(true); expect(hasDenied).toBe(true);