()
| 1932 | // ask() call so messages that queued up during a long turn coalesce |
| 1933 | // into a single follow-up turn instead of N separate turns. |
| 1934 | const drainCommandQueue = async () => { |
| 1935 | while ((command = dequeue(isMainThread))) { |
| 1936 | if ( |
| 1937 | command.mode !== 'prompt' && |
| 1938 | command.mode !== 'orphaned-permission' && |
| 1939 | command.mode !== 'task-notification' |
| 1940 | ) { |
| 1941 | throw new Error( |
| 1942 | 'only prompt commands are supported in streaming mode', |
| 1943 | ) |
| 1944 | } |
| 1945 | |
| 1946 | // Non-prompt commands (task-notification, orphaned-permission) carry |
| 1947 | // side effects or orphanedPermission state, so they process singly. |
| 1948 | // Prompt commands greedily collect followers with matching workload. |
| 1949 | const batch: QueuedCommand[] = [command] |
| 1950 | if (command.mode === 'prompt') { |
| 1951 | while (canBatchWith(command, peek(isMainThread))) { |
| 1952 | batch.push(dequeue(isMainThread)!) |
| 1953 | } |
| 1954 | if (batch.length > 1) { |
| 1955 | command = { |
| 1956 | ...command, |
| 1957 | value: joinPromptValues(batch.map(c => c.value)), |
| 1958 | uuid: batch.findLast(c => c.uuid)?.uuid ?? command.uuid, |
| 1959 | } |
| 1960 | } |
| 1961 | } |
| 1962 | const batchUuids = batch.map(c => c.uuid).filter(u => u !== undefined) |
| 1963 | |
| 1964 | // QueryEngine will emit a replay for command.uuid (the last uuid in |
| 1965 | // the batch) via its messagesToAck path. Emit replays here for the |
| 1966 | // rest so consumers that track per-uuid delivery (clank's |
| 1967 | // asyncMessages footer, CCR) see an ack for every message they sent, |
| 1968 | // not just the one that survived the merge. |
| 1969 | if (options.replayUserMessages && batch.length > 1) { |
| 1970 | for (const c of batch) { |
| 1971 | if (c.uuid && c.uuid !== command.uuid) { |
| 1972 | output.enqueue({ |
| 1973 | type: 'user', |
| 1974 | message: { role: 'user', content: c.value }, |
| 1975 | session_id: getSessionId(), |
| 1976 | parent_tool_use_id: null, |
| 1977 | uuid: c.uuid, |
| 1978 | isReplay: true, |
| 1979 | } satisfies SDKUserMessageReplay) |
| 1980 | } |
| 1981 | } |
| 1982 | } |
| 1983 | |
| 1984 | // Combine all MCP clients. appState.mcp is populated incrementally |
| 1985 | // per-server by main.tsx (mirrors useManageMCPConnections). Reading |
| 1986 | // fresh per-command means late-connecting servers are visible on the |
| 1987 | // next turn. registerElicitationHandlers is idempotent (tracking set). |
| 1988 | const appState = getAppState() |
| 1989 | const allMcpClients = [ |
| 1990 | ...appState.mcp.clients, |
| 1991 | ...sdkClients, |
no test coverage detected