diff --git a/apps/desktop/src/main/services/chat/agentChatService.test.ts b/apps/desktop/src/main/services/chat/agentChatService.test.ts index 0ce548b80..e92bc8922 100644 --- a/apps/desktop/src/main/services/chat/agentChatService.test.ts +++ b/apps/desktop/src/main/services/chat/agentChatService.test.ts @@ -3181,6 +3181,83 @@ describe("createAgentChatService", () => { }, { timeout: 2000, interval: 50 }); }); + it("sends Codex brief handoff text before syncing the inherited goal", async () => { + const { service, sessionService } = createService(); + const source = await service.createSession({ + laneId: "lane-1", + provider: "codex", + model: "gpt-5.5", + modelId: "openai/gpt-5.5", + }); + sessionService.updateMeta({ + sessionId: source.id, + goal: "No Machine State Polish", + }); + const sourceRow = mockState.sessions.get(source.id); + if (sourceRow) { + sourceRow.summary = "Fix the iPhone 17 simulator chat layout handoff."; + } + + const handoffStart = mockState.codexRequestPayloads.length; + const result = await service.handoffSession({ + sourceSessionId: source.id, + targetModelId: "openai/gpt-5.5", + }); + + expect(result.session.provider).toBe("codex"); + expect(mockState.sessions.get(result.session.id)?.goal).toBe("No Machine State Polish"); + + const handoffPayloads = mockState.codexRequestPayloads.slice(handoffStart); + const requestMethods = handoffPayloads.map((payload) => String(payload.method ?? "")); + const turnStartIndex = requestMethods.indexOf("turn/start"); + const goalSetIndex = requestMethods.indexOf("thread/goal/set"); + expect(turnStartIndex).toBeGreaterThanOrEqual(0); + expect(goalSetIndex).toBeGreaterThan(turnStartIndex); + + const turnStartRequest = handoffPayloads[turnStartIndex] as { + params?: { input?: Array<{ text?: unknown }> }; + }; + const inputText = turnStartRequest.params?.input?.map((entry) => String(entry.text ?? "")).join("\n") ?? ""; + expect(inputText).toContain("This message was injected automatically by ADE during a chat handoff."); + expect(inputText).toContain("No Machine State Polish"); + + const goalSetRequest = handoffPayloads[goalSetIndex] as { + params?: { objective?: unknown }; + }; + expect(goalSetRequest.params?.objective).toBe("No Machine State Polish"); + }); + + it("keeps Codex brief handoff successful when deferred goal seeding throws", async () => { + const { service, sessionService } = createService(); + const source = await service.createSession({ + laneId: "lane-1", + provider: "codex", + model: "gpt-5.5", + modelId: "openai/gpt-5.5", + }); + sessionService.updateMeta({ + sessionId: source.id, + goal: "No Machine State Polish", + }); + mockState.codexResponseOverrides.set("thread/goal/set", () => { + throw new Error("goal seed unavailable"); + }); + + const handoffStart = mockState.codexRequestPayloads.length; + const result = await service.handoffSession({ + sourceSessionId: source.id, + targetModelId: "openai/gpt-5.5", + }); + + expect(result.session.provider).toBe("codex"); + expect(mockState.sessions.get(result.session.id)?.goal).toBe("No Machine State Polish"); + const handoffMethods = mockState.codexRequestPayloads + .slice(handoffStart) + .map((payload) => String(payload.method ?? "")); + expect(handoffMethods).toContain("turn/start"); + expect(handoffMethods).toContain("thread/goal/set"); + }); + it("uses the selected Claude handoff permission instead of the source interaction mode", async () => { const send = vi.fn().mockResolvedValue(undefined); const setPermissionMode = vi.fn().mockResolvedValue(undefined); diff --git a/apps/desktop/src/main/services/chat/agentChatService.ts b/apps/desktop/src/main/services/chat/agentChatService.ts index eb33ec304..4be2e56c8 100644 --- a/apps/desktop/src/main/services/chat/agentChatService.ts +++ b/apps/desktop/src/main/services/chat/agentChatService.ts @@ -19970,27 +19970,51 @@ export function createAgentChatService(args: { const inheritedGoal = trimLine(sourceSession.goal) ?? trimLine(sourceSession.summary) ?? trimLine(sourceSession.title); - if (inheritedGoal) { + const applyInheritedGoal = (): void => { + if (!inheritedGoal) return; createdManaged.session.goal = inheritedGoal; sessionService.updateMeta({ sessionId: created.id, goal: inheritedGoal, }); + }; + const deferInheritedGoalUntilHandoffDispatch = + handoffMode === "brief" && createdManaged.session.provider === "codex"; + if (!deferInheritedGoalUntilHandoffDispatch) { + applyInheritedGoal(); } persistChatState(createdManaged); if (handoffMode === "brief") { - await sendMessage({ - sessionId: created.id, - text: buildHandoffPrompt(brief), - displayText: "Chat handoff from previous session", - metadata: { kind: "handoff", hideFullPrompt: true }, - reasoningEffort: targetReasoningEffort, - executionMode: createdManaged.session.executionMode ?? null, - interactionMode: createdManaged.session.interactionMode ?? null, - }, { - awaitDispatch: true, - }); + try { + await sendMessage({ + sessionId: created.id, + text: buildHandoffPrompt(brief), + displayText: "Chat handoff from previous session", + metadata: { kind: "handoff", hideFullPrompt: true }, + reasoningEffort: targetReasoningEffort, + executionMode: createdManaged.session.executionMode ?? null, + interactionMode: createdManaged.session.interactionMode ?? null, + }, { + awaitDispatch: true, + }); + } finally { + if (deferInheritedGoalUntilHandoffDispatch) { + applyInheritedGoal(); + persistChatState(createdManaged); + if (createdManaged.runtime?.kind === "codex") { + try { + await seedCodexThreadGoalFromSessionGoal(createdManaged, createdManaged.runtime); + } catch (error) { + logger.warn("agent_chat.codex_goal_seed_after_handoff_failed", { + sessionId: createdManaged.session.id, + error: error instanceof Error ? error.message : String(error), + }); + persistChatState(createdManaged); + } + } + } + } } return { diff --git a/apps/desktop/src/renderer/components/files/v2/EditorGroup.test.tsx b/apps/desktop/src/renderer/components/files/v2/EditorGroup.test.tsx index a86cc5f55..cee341de4 100644 --- a/apps/desktop/src/renderer/components/files/v2/EditorGroup.test.tsx +++ b/apps/desktop/src/renderer/components/files/v2/EditorGroup.test.tsx @@ -27,6 +27,7 @@ const registry = { } as unknown as MonacoModelRegistry; const tabId = editorTabId("workspace-1", "src/file.ts"); +const otherLaneTabId = editorTabId("workspace-2", "src/other.ts"); const baseProps: EditorGroupProps = { group: { @@ -108,6 +109,36 @@ describe("EditorGroup", () => { expect(screen.getByTestId("viewer-button")).toBeTruthy(); }); + it("marks the visible fallback tab active when lane scope hides the stored active tab", () => { + render( + , + ); + + expect(screen.getByRole("tab", { name: /file\.ts/i }).getAttribute("aria-selected")).toBe("true"); + expect(screen.queryByRole("tab", { name: /other\.ts/i })).toBeNull(); + }); + it("does not steal Cmd+S from focused text inputs", () => { render(); const input = screen.getByTestId("viewer-input"); diff --git a/apps/desktop/src/renderer/components/files/v2/EditorGroup.tsx b/apps/desktop/src/renderer/components/files/v2/EditorGroup.tsx index c019748eb..27a933e1a 100644 --- a/apps/desktop/src/renderer/components/files/v2/EditorGroup.tsx +++ b/apps/desktop/src/renderer/components/files/v2/EditorGroup.tsx @@ -178,7 +178,7 @@ export function EditorGroup(props: EditorGroupProps) {