Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions apps/desktop/src/main/services/chat/agentChatService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
48 changes: 36 additions & 12 deletions apps/desktop/src/main/services/chat/agentChatService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
} catch (error) {
logger.warn("agent_chat.codex_goal_seed_after_handoff_failed", {
Comment thread
greptile-apps[bot] marked this conversation as resolved.
sessionId: createdManaged.session.id,
error: error instanceof Error ? error.message : String(error),
});
persistChatState(createdManaged);
}
}
}
}
}

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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(
<EditorGroup
{...baseProps}
tabScope="lane"
group={{
...baseProps.group,
activeTabId: otherLaneTabId,
tabs: [
...baseProps.group.tabs,
{
id: otherLaneTabId,
workspaceId: "workspace-2",
laneId: "lane-2",
path: "src/other.ts",
title: "other.ts",
viewerKind: "code",
languageId: "typescript",
preview: false,
pinned: false,
},
],
}}
/>,
);

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(<EditorGroup {...baseProps} />);
const input = screen.getByTestId("viewer-input");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ export function EditorGroup(props: EditorGroupProps) {
<TabButton
key={tab.id}
tab={tab}
active={tab.id === group.activeTabId}
active={tab.id === activeTab?.id}
dirty={dirtyTabIds.has(tab.id)}
laneAccent={props.tabScope === "all" ? laneAccentForTab(tab, props.lanes) : undefined}
showLaneDivider={props.tabScope === "all" && isLaneGroupBoundary(displayTabs, index)}
Expand Down
Loading