queueProcessor.ts 3.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import type { QueuedCommand } from '../types/textInputTypes.js'
  2. import {
  3. dequeue,
  4. dequeueAllMatching,
  5. hasCommandsInQueue,
  6. peek,
  7. } from './messageQueueManager.js'
  8. type ProcessQueueParams = {
  9. executeInput: (commands: QueuedCommand[]) => Promise<void>
  10. }
  11. type ProcessQueueResult = {
  12. processed: boolean
  13. }
  14. /**
  15. * Check if a queued command is a slash command (value starts with '/').
  16. */
  17. function isSlashCommand(cmd: QueuedCommand): boolean {
  18. if (typeof cmd.value === 'string') {
  19. return cmd.value.trim().startsWith('/')
  20. }
  21. // For ContentBlockParam[], check the first text block
  22. for (const block of cmd.value) {
  23. if (block.type === 'text') {
  24. return block.text.trim().startsWith('/')
  25. }
  26. }
  27. return false
  28. }
  29. /**
  30. * Processes commands from the queue.
  31. *
  32. * Slash commands (starting with '/') and bash-mode commands are processed
  33. * one at a time so each goes through the executeInput path individually.
  34. * Bash commands need individual processing to preserve per-command error
  35. * isolation, exit codes, and progress UI. Other non-slash commands are
  36. * batched: all items **with the same mode** as the highest-priority item
  37. * are drained at once and passed as a single array to executeInput — each
  38. * becomes its own user message with its own UUID. Different modes
  39. * (e.g. prompt vs task-notification) are never mixed because they are
  40. * treated differently downstream.
  41. *
  42. * The caller is responsible for ensuring no query is currently running
  43. * and for calling this function again after each command completes
  44. * until the queue is empty.
  45. *
  46. * @returns result with processed status
  47. */
  48. export function processQueueIfReady({
  49. executeInput,
  50. }: ProcessQueueParams): ProcessQueueResult {
  51. // This processor runs on the REPL main thread between turns. Skip anything
  52. // addressed to a subagent — an unfiltered peek() returning a subagent
  53. // notification would set targetMode, dequeueAllMatching would find nothing
  54. // matching that mode with agentId===undefined, and we'd return processed:
  55. // false with the queue unchanged → the React effect never re-fires and any
  56. // queued user prompt stalls permanently.
  57. const isMainThread = (cmd: QueuedCommand) => cmd.agentId === undefined
  58. const next = peek(isMainThread)
  59. if (!next) {
  60. return { processed: false }
  61. }
  62. // Slash commands and bash-mode commands are processed individually.
  63. // Bash commands need per-command error isolation, exit codes, and progress UI.
  64. if (isSlashCommand(next) || next.mode === 'bash') {
  65. const cmd = dequeue(isMainThread)!
  66. void executeInput([cmd])
  67. return { processed: true }
  68. }
  69. // Drain all non-slash-command items with the same mode at once.
  70. const targetMode = next.mode
  71. const commands = dequeueAllMatching(
  72. cmd => isMainThread(cmd) && !isSlashCommand(cmd) && cmd.mode === targetMode,
  73. )
  74. if (commands.length === 0) {
  75. return { processed: false }
  76. }
  77. void executeInput(commands)
  78. return { processed: true }
  79. }
  80. /**
  81. * Checks if the queue has pending commands.
  82. * Use this to determine if queue processing should be triggered.
  83. */
  84. export function hasQueuedCommands(): boolean {
  85. return hasCommandsInQueue()
  86. }