| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495 |
- import type { QueuedCommand } from '../types/textInputTypes.js'
- import {
- dequeue,
- dequeueAllMatching,
- hasCommandsInQueue,
- peek,
- } from './messageQueueManager.js'
- type ProcessQueueParams = {
- executeInput: (commands: QueuedCommand[]) => Promise<void>
- }
- type ProcessQueueResult = {
- processed: boolean
- }
- /**
- * Check if a queued command is a slash command (value starts with '/').
- */
- function isSlashCommand(cmd: QueuedCommand): boolean {
- if (typeof cmd.value === 'string') {
- return cmd.value.trim().startsWith('/')
- }
- // For ContentBlockParam[], check the first text block
- for (const block of cmd.value) {
- if (block.type === 'text') {
- return block.text.trim().startsWith('/')
- }
- }
- return false
- }
- /**
- * Processes commands from the queue.
- *
- * Slash commands (starting with '/') and bash-mode commands are processed
- * one at a time so each goes through the executeInput path individually.
- * Bash commands need individual processing to preserve per-command error
- * isolation, exit codes, and progress UI. Other non-slash commands are
- * batched: all items **with the same mode** as the highest-priority item
- * are drained at once and passed as a single array to executeInput — each
- * becomes its own user message with its own UUID. Different modes
- * (e.g. prompt vs task-notification) are never mixed because they are
- * treated differently downstream.
- *
- * The caller is responsible for ensuring no query is currently running
- * and for calling this function again after each command completes
- * until the queue is empty.
- *
- * @returns result with processed status
- */
- export function processQueueIfReady({
- executeInput,
- }: ProcessQueueParams): ProcessQueueResult {
- // This processor runs on the REPL main thread between turns. Skip anything
- // addressed to a subagent — an unfiltered peek() returning a subagent
- // notification would set targetMode, dequeueAllMatching would find nothing
- // matching that mode with agentId===undefined, and we'd return processed:
- // false with the queue unchanged → the React effect never re-fires and any
- // queued user prompt stalls permanently.
- const isMainThread = (cmd: QueuedCommand) => cmd.agentId === undefined
- const next = peek(isMainThread)
- if (!next) {
- return { processed: false }
- }
- // Slash commands and bash-mode commands are processed individually.
- // Bash commands need per-command error isolation, exit codes, and progress UI.
- if (isSlashCommand(next) || next.mode === 'bash') {
- const cmd = dequeue(isMainThread)!
- void executeInput([cmd])
- return { processed: true }
- }
- // Drain all non-slash-command items with the same mode at once.
- const targetMode = next.mode
- const commands = dequeueAllMatching(
- cmd => isMainThread(cmd) && !isSlashCommand(cmd) && cmd.mode === targetMode,
- )
- if (commands.length === 0) {
- return { processed: false }
- }
- void executeInput(commands)
- return { processed: true }
- }
- /**
- * Checks if the queue has pending commands.
- * Use this to determine if queue processing should be triggered.
- */
- export function hasQueuedCommands(): boolean {
- return hasCommandsInQueue()
- }
|