fix(app): resolve create agent dictation confirm handler timing issue

Fixed a state management bug where clicking the checkmark in create agent dictation mode would not trigger processing. The issue was that setIsDictationProcessing(true) was called after stopping the recorder, causing a race condition where the UI never showed the processing state.

Changes:
- Move setIsDictationProcessing(true) to execute immediately at the start of the confirm handler
- Add proper cleanup when audioData is null
- Ensure UI shows loading spinner when checkmark is clicked

This aligns the create agent dictation behavior with the working agent chat dictation implementation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-11-28 10:10:31 +00:00
co-authored by Claude
parent b97ae99b2b
commit 2c84447ec2
10 changed files with 694 additions and 188 deletions
@@ -68,6 +68,15 @@ export type AgentManagerOptions = {
idFactory?: () => string;
};
export type WaitForAgentOptions = {
signal?: AbortSignal;
};
export type WaitForAgentResult = {
status: AgentLifecycleStatus;
permission: AgentPermissionRequest | null;
};
type ManagedAgent = {
id: string;
provider: AgentProvider;
@@ -97,6 +106,28 @@ type SubscriptionRecord = {
};
const DEFAULT_MAX_TIMELINE_ITEMS = 2000;
const BUSY_STATUSES: AgentLifecycleStatus[] = [
"initializing",
"running",
];
function isAgentBusy(status: AgentLifecycleStatus): boolean {
return BUSY_STATUSES.includes(status);
}
function createAbortError(
signal: AbortSignal | undefined,
fallbackMessage: string
): Error {
const reason = signal?.reason;
const message =
typeof reason === "string"
? reason
: reason instanceof Error
? reason.message
: fallbackMessage;
return Object.assign(new Error(message), { name: "AbortError" });
}
export class AgentManager {
private readonly clients = new Map<AgentProvider, AgentClient>();
@@ -460,6 +491,105 @@ export class AgentManager {
await this.primeHistory(agent);
}
async waitForAgentEvent(
agentId: string,
options?: WaitForAgentOptions
): Promise<WaitForAgentResult> {
const snapshot = this.getAgent(agentId);
if (!snapshot) {
throw new Error(`Agent ${agentId} not found`);
}
const immediatePermission = snapshot.pendingPermissions[0] ?? null;
if (immediatePermission) {
return { status: snapshot.status, permission: immediatePermission };
}
if (!isAgentBusy(snapshot.status)) {
return { status: snapshot.status, permission: null };
}
if (options?.signal?.aborted) {
throw createAbortError(options.signal, "wait_for_agent aborted");
}
return await new Promise<WaitForAgentResult>((resolve, reject) => {
let currentStatus: AgentLifecycleStatus = snapshot.status;
const cleanupFns: Array<() => void> = [];
const cleanup = () => {
while (cleanupFns.length) {
const fn = cleanupFns.pop();
try {
fn?.();
} catch {
// ignore cleanup errors
}
}
};
const finish = (permission: AgentPermissionRequest | null) => {
cleanup();
resolve({ status: currentStatus, permission });
};
const unsubscribe = this.subscribe(
(event) => {
if (event.type === "agent_state") {
currentStatus = event.agent.status;
const pending = event.agent.pendingPermissions[0] ?? null;
if (pending) {
finish(pending);
return;
}
if (!isAgentBusy(event.agent.status)) {
finish(null);
}
return;
}
if (event.type !== "agent_stream") {
return;
}
switch (event.event.type) {
case "permission_requested": {
currentStatus = "running";
finish(event.event.request);
break;
}
case "turn_completed": {
currentStatus = "idle";
finish(null);
break;
}
case "turn_failed": {
currentStatus = "error";
finish(null);
break;
}
default:
break;
}
},
{ agentId, replayState: true }
);
cleanupFns.push(unsubscribe);
if (options?.signal) {
const abortHandler = () => {
cleanup();
reject(createAbortError(options.signal, "wait_for_agent aborted"));
};
options.signal.addEventListener("abort", abortHandler, { once: true });
cleanupFns.push(() =>
options.signal?.removeEventListener("abort", abortHandler)
);
}
});
}
private async registerSession(
session: AgentSession,
config: AgentSessionConfig,