Skip to content

Commit d0c33f0

Browse files
committed
Agent chat improvements and lane base tracking
Introduce several agent chat/runtime improvements and refine lane/base tracking for rebases. Key changes: - Agent chat: add persisted approvalOverrides for Claude sessions, normalize tool names, and centralize initial turn activity via initialTurnActivity to emit an immediate startup activity before stream output arrives. Tests added to verify startup activity for unified and Claude streams. - Approval flow: avoid hanging promises by using try/finally when registering runtime.approvals and pending elicitations, and drain pending approvals on interrupt. Persist and restore approvalOverrides in session state. - Claude handling: refactor tool headline generation, synthesize todo updates via maybeEmitTodoUpdate, and tighten URL openExternal to only allow http/https. - Conflict/lanes: use shouldLaneTrackParent and branchNameFromLaneRef to determine tracked parents; add explicit RebaseNeed.kind ("lane_base" | "pr_target") and return both need types (remove prior deduplication). Update auto-rebase logic to respect tracked parents and skip legacy parent links. Add tests to cover the new behaviors. - IPC: include laneCount in recent project summaries. - Misc: various test additions and small refactors across services to support the above. These changes fix race/hang issues around approvals, make startup activity more predictable, and improve correctness when parent lanes diverge from stored bases.
1 parent 6f50999 commit d0c33f0

84 files changed

Lines changed: 6618 additions & 1445 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.ade/state.sqlite

Whitespace-only changes.

.claude/commands/automate.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,10 @@ cd apps/desktop && npx vitest run [affected existing test files]
271271

272272
### 6a. Check vitest workspace config
273273

274-
Read `apps/desktop/vitest.workspace.ts` and verify every new test file matches an include pattern:
275-
- Unit: `src/**/*.test.{ts,tsx}`
274+
Read `apps/desktop/vitest.workspace.ts` and verify every new test file matches one of the three workspace project include patterns:
275+
- `unit-main`: `src/main/**/*.test.{ts,tsx}`
276+
- `unit-renderer`: `src/renderer/**/*.test.{ts,tsx}`
277+
- `unit-shared`: `src/shared/**/*.test.{ts,tsx}` and `src/preload/**/*.test.{ts,tsx}`
276278

277279
If a test file does NOT match, update the workspace config.
278280

apps/desktop/src/main/services/chat/agentChatService.test.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3313,4 +3313,133 @@ describe("createAgentChatService", () => {
33133313
expect(updated?.unifiedPermissionMode).toBe("edit");
33143314
});
33153315
});
3316+
3317+
it("emits immediate startup activity before unified stream output arrives", async () => {
3318+
const events: AgentChatEventEnvelope[] = [];
3319+
let releaseStream!: () => void;
3320+
const streamGate = new Promise<void>((resolve) => {
3321+
releaseStream = () => resolve();
3322+
});
3323+
3324+
vi.mocked(streamText).mockImplementation(() => ({
3325+
fullStream: (async function* () {
3326+
await streamGate;
3327+
yield { type: "finish", usage: {} };
3328+
})(),
3329+
}) as any);
3330+
3331+
const { service } = createService({
3332+
onEvent: (event: AgentChatEventEnvelope) => events.push(event),
3333+
});
3334+
3335+
const session = await service.createSession({
3336+
laneId: "lane-1",
3337+
provider: "unified",
3338+
model: "openai/gpt-5.4",
3339+
modelId: "openai/gpt-5.4",
3340+
});
3341+
3342+
const sendPromise = service.sendMessage({
3343+
sessionId: session.id,
3344+
text: "Resolve the PR comments.",
3345+
});
3346+
3347+
const startedEvent = await waitForEvent(
3348+
events,
3349+
(event): event is AgentChatEventEnvelope & {
3350+
event: Extract<AgentChatEventEnvelope["event"], { type: "status" }>;
3351+
} => event.event.type === "status" && event.event.turnStatus === "started",
3352+
);
3353+
3354+
const startupActivity = await waitForEvent(
3355+
events,
3356+
(event): event is AgentChatEventEnvelope & {
3357+
event: Extract<AgentChatEventEnvelope["event"], { type: "activity" }>;
3358+
} =>
3359+
event.event.type === "activity"
3360+
&& event.event.turnId === startedEvent.event.turnId
3361+
&& (event.event.activity === "thinking" || event.event.activity === "working"),
3362+
);
3363+
3364+
expect(startupActivity.event.detail).toBeTruthy();
3365+
3366+
releaseStream();
3367+
await sendPromise;
3368+
});
3369+
3370+
it("emits immediate startup activity before Claude SDK stream output arrives", async () => {
3371+
const events: AgentChatEventEnvelope[] = [];
3372+
const setPermissionMode = vi.fn().mockResolvedValue(undefined);
3373+
const send = vi.fn().mockResolvedValue(undefined);
3374+
let streamCall = 0;
3375+
let releaseStream!: () => void;
3376+
const streamGate = new Promise<void>((resolve) => {
3377+
releaseStream = () => resolve();
3378+
});
3379+
3380+
const stream = vi.fn(() => (async function* () {
3381+
streamCall += 1;
3382+
if (streamCall === 1) {
3383+
yield {
3384+
type: "system",
3385+
subtype: "init",
3386+
session_id: "sdk-session-1",
3387+
slash_commands: [],
3388+
};
3389+
return;
3390+
}
3391+
3392+
await streamGate;
3393+
yield {
3394+
type: "result",
3395+
usage: { input_tokens: 1, output_tokens: 1 },
3396+
};
3397+
})());
3398+
3399+
vi.mocked(unstable_v2_createSession).mockReturnValue({
3400+
send,
3401+
stream,
3402+
close: vi.fn(),
3403+
sessionId: "sdk-session-1",
3404+
setPermissionMode,
3405+
} as any);
3406+
3407+
const { service } = createService({
3408+
onEvent: (event: AgentChatEventEnvelope) => events.push(event),
3409+
});
3410+
3411+
const session = await service.createSession({
3412+
laneId: "lane-1",
3413+
provider: "claude",
3414+
model: "claude-sonnet-4-6",
3415+
modelId: "anthropic/claude-sonnet-4-6",
3416+
});
3417+
3418+
const sendPromise = service.sendMessage({
3419+
sessionId: session.id,
3420+
text: "Resolve the PR comments.",
3421+
});
3422+
3423+
const startedEvent = await waitForEvent(
3424+
events,
3425+
(event): event is AgentChatEventEnvelope & {
3426+
event: Extract<AgentChatEventEnvelope["event"], { type: "status" }>;
3427+
} => event.event.type === "status" && event.event.turnStatus === "started",
3428+
);
3429+
3430+
const startupActivity = await waitForEvent(
3431+
events,
3432+
(event): event is AgentChatEventEnvelope & {
3433+
event: Extract<AgentChatEventEnvelope["event"], { type: "activity" }>;
3434+
} =>
3435+
event.event.type === "activity"
3436+
&& event.event.turnId === startedEvent.event.turnId
3437+
&& (event.event.activity === "thinking" || event.event.activity === "working"),
3438+
);
3439+
3440+
expect(startupActivity.event.detail).toBeTruthy();
3441+
3442+
releaseStream();
3443+
await sendPromise;
3444+
});
33163445
});

0 commit comments

Comments
 (0)