{"version":3,"file":"message-queue-controller.d.ts","sourceRoot":"","sources":["../../../src/modes/interactive/message-queue-controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,eAAe,EAAyB,MAAM,0BAA0B,CAAC;AACvG,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAShE,iEAAiE;AACjE,MAAM,WAAW,0BAA0B;IAC1C,0EAA0E;IAC1E,IAAI,OAAO,IAAI,YAAY,CAAC;IAC5B,wEAAwE;IACxE,SAAS,IAAI,eAAe,CAAC;IAC7B,uEAAuE;IACvE,wBAAwB,EAAE,SAAS,CAAC;IACpC,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,SAAS,CAAC,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CACtC;AAED,qBAAa,sBAAsB;IAItB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAFjC,OAAO,CAAC,wBAAwB,CAAiC;IAEjE,YAA6B,IAAI,EAAE,0BAA0B,EAAI;IAEjE,OAAO,KAAK,OAAO,GAElB;IAED,OAAO,KAAK,MAAM,GAEjB;IAED,6EAA6E;IAC7E,oBAAoB,IAAI,IAAI,CAE3B;IAED,OAAO,CAAC,oBAAoB;IAa5B;;;OAGG;IACH,OAAO,CAAC,cAAc;IAetB,4BAA4B,IAAI,IAAI,CAiBnC;IAED,6BAA6B,CAAC,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAmBzF;IAED,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,UAAU,GAAG,IAAI,CAMrE;IAED,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAQxC;IAEK,oBAAoB,CAAC,OAAO,CAAC,EAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CA2E3E;CACD","sourcesContent":["/**\n * Message queueing for the interactive mode.\n *\n * Owns the compaction queue (messages typed while a compaction is running) and\n * the pending-messages display above the editor. The session owns the live\n * steering / follow-up queues; this controller merges them with the compaction\n * queue for display, restores everything back into the editor on demand, and\n * flushes the compaction queue once compaction finishes. Extracted from\n * interactive-mode.ts behind a narrow MessageQueueControllerDeps interface.\n */\n\nimport { type Container, type EditorComponent, Spacer, TruncatedText } from \"@kolisachint/hoocode-tui\";\nimport type { AgentSession } from \"../../core/agent-session.js\";\nimport { keyDisplayText } from \"./components/keybinding-hints.js\";\nimport { theme } from \"./theme/theme.js\";\n\ntype CompactionQueuedMessage = {\n\ttext: string;\n\tmode: \"steer\" | \"followUp\";\n};\n\n/** The slice of the interactive mode the message queue needs. */\nexport interface MessageQueueControllerDeps {\n\t/** The active session (read at call time; the session can be swapped). */\n\tget session(): AgentSession;\n\t/** The prompt editor (read at call time; the editor can be swapped). */\n\tgetEditor(): EditorComponent;\n\t/** Container that renders the queued-message list above the editor. */\n\tpendingMessagesContainer: Container;\n\tshowStatus(message: string): void;\n\tshowError(errorMessage: string): void;\n}\n\nexport class MessageQueueController {\n\t// Messages queued while compaction is running\n\tprivate compactionQueuedMessages: CompactionQueuedMessage[] = [];\n\n\tconstructor(private readonly deps: MessageQueueControllerDeps) {}\n\n\tprivate get session(): AgentSession {\n\t\treturn this.deps.session;\n\t}\n\n\tprivate get editor(): EditorComponent {\n\t\treturn this.deps.getEditor();\n\t}\n\n\t/** Drop the compaction queue without restoring it (session switch/reset). */\n\tresetCompactionQueue(): void {\n\t\tthis.compactionQueuedMessages = [];\n\t}\n\n\tprivate getAllQueuedMessages(): { steering: string[]; followUp: string[] } {\n\t\treturn {\n\t\t\tsteering: [\n\t\t\t\t...this.session.getSteeringMessages(),\n\t\t\t\t...this.compactionQueuedMessages.filter((msg) => msg.mode === \"steer\").map((msg) => msg.text),\n\t\t\t],\n\t\t\tfollowUp: [\n\t\t\t\t...this.session.getFollowUpMessages(),\n\t\t\t\t...this.compactionQueuedMessages.filter((msg) => msg.mode === \"followUp\").map((msg) => msg.text),\n\t\t\t],\n\t\t};\n\t}\n\n\t/**\n\t * Clear all queued messages and return their contents.\n\t * Clears both session queue and compaction queue.\n\t */\n\tprivate clearAllQueues(): { steering: string[]; followUp: string[] } {\n\t\tconst { steering, followUp } = this.session.clearQueue();\n\t\tconst compactionSteering = this.compactionQueuedMessages\n\t\t\t.filter((msg) => msg.mode === \"steer\")\n\t\t\t.map((msg) => msg.text);\n\t\tconst compactionFollowUp = this.compactionQueuedMessages\n\t\t\t.filter((msg) => msg.mode === \"followUp\")\n\t\t\t.map((msg) => msg.text);\n\t\tthis.compactionQueuedMessages = [];\n\t\treturn {\n\t\t\tsteering: [...steering, ...compactionSteering],\n\t\t\tfollowUp: [...followUp, ...compactionFollowUp],\n\t\t};\n\t}\n\n\tupdatePendingMessagesDisplay(): void {\n\t\tthis.deps.pendingMessagesContainer.clear();\n\t\tconst { steering: steeringMessages, followUp: followUpMessages } = this.getAllQueuedMessages();\n\t\tif (steeringMessages.length > 0 || followUpMessages.length > 0) {\n\t\t\tthis.deps.pendingMessagesContainer.addChild(new Spacer(1));\n\t\t\tfor (const message of steeringMessages) {\n\t\t\t\tconst text = theme.fg(\"dim\", `Steering: ${message}`);\n\t\t\t\tthis.deps.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0));\n\t\t\t}\n\t\t\tfor (const message of followUpMessages) {\n\t\t\t\tconst text = theme.fg(\"dim\", `Follow-up: ${message}`);\n\t\t\t\tthis.deps.pendingMessagesContainer.addChild(new TruncatedText(text, 1, 0));\n\t\t\t}\n\t\t\tconst dequeueHint = keyDisplayText(\"app.message.dequeue\");\n\t\t\tconst hintText = theme.fg(\"dim\", `↳ ${dequeueHint} to edit all queued messages`);\n\t\t\tthis.deps.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));\n\t\t}\n\t}\n\n\trestoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {\n\t\tconst { steering, followUp } = this.clearAllQueues();\n\t\tconst allQueued = [...steering, ...followUp];\n\t\tif (allQueued.length === 0) {\n\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\tif (options?.abort) {\n\t\t\t\tthis.session.agent.abort();\n\t\t\t}\n\t\t\treturn 0;\n\t\t}\n\t\tconst queuedText = allQueued.join(\"\\n\\n\");\n\t\tconst currentText = options?.currentText ?? this.editor.getText();\n\t\tconst combinedText = [queuedText, currentText].filter((t) => t.trim()).join(\"\\n\\n\");\n\t\tthis.editor.setText(combinedText);\n\t\tthis.updatePendingMessagesDisplay();\n\t\tif (options?.abort) {\n\t\t\tthis.session.agent.abort();\n\t\t}\n\t\treturn allQueued.length;\n\t}\n\n\tqueueCompactionMessage(text: string, mode: \"steer\" | \"followUp\"): void {\n\t\tthis.compactionQueuedMessages.push({ text, mode });\n\t\tthis.editor.addToHistory?.(text);\n\t\tthis.editor.setText(\"\");\n\t\tthis.updatePendingMessagesDisplay();\n\t\tthis.deps.showStatus(\"Queued message for after compaction\");\n\t}\n\n\tisExtensionCommand(text: string): boolean {\n\t\tif (!text.startsWith(\"/\")) return false;\n\n\t\tconst extensionRunner = this.session.extensionRunner;\n\n\t\tconst spaceIndex = text.indexOf(\" \");\n\t\tconst commandName = spaceIndex === -1 ? text.slice(1) : text.slice(1, spaceIndex);\n\t\treturn !!extensionRunner.getCommand(commandName);\n\t}\n\n\tasync flushCompactionQueue(options?: { willRetry?: boolean }): Promise<void> {\n\t\tif (this.compactionQueuedMessages.length === 0) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst queuedMessages = [...this.compactionQueuedMessages];\n\t\tthis.compactionQueuedMessages = [];\n\t\tthis.updatePendingMessagesDisplay();\n\n\t\tconst restoreQueue = (error: unknown) => {\n\t\t\tthis.session.clearQueue();\n\t\t\tthis.compactionQueuedMessages = queuedMessages;\n\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\tthis.deps.showError(\n\t\t\t\t`Failed to send queued message${queuedMessages.length > 1 ? \"s\" : \"\"}: ${\n\t\t\t\t\terror instanceof Error ? error.message : String(error)\n\t\t\t\t}`,\n\t\t\t);\n\t\t};\n\n\t\ttry {\n\t\t\tif (options?.willRetry) {\n\t\t\t\t// When retry is pending, queue messages for the retry turn\n\t\t\t\tfor (const message of queuedMessages) {\n\t\t\t\t\tif (this.isExtensionCommand(message.text)) {\n\t\t\t\t\t\tawait this.session.prompt(message.text);\n\t\t\t\t\t} else if (message.mode === \"followUp\") {\n\t\t\t\t\t\tawait this.session.followUp(message.text);\n\t\t\t\t\t} else {\n\t\t\t\t\t\tawait this.session.steer(message.text);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Find first non-extension-command message to use as prompt\n\t\t\tconst firstPromptIndex = queuedMessages.findIndex((message) => !this.isExtensionCommand(message.text));\n\t\t\tif (firstPromptIndex === -1) {\n\t\t\t\t// All extension commands - execute them all\n\t\t\t\tfor (const message of queuedMessages) {\n\t\t\t\t\tawait this.session.prompt(message.text);\n\t\t\t\t}\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\t// Execute any extension commands before the first prompt\n\t\t\tconst preCommands = queuedMessages.slice(0, firstPromptIndex);\n\t\t\tconst firstPrompt = queuedMessages[firstPromptIndex];\n\t\t\tconst rest = queuedMessages.slice(firstPromptIndex + 1);\n\n\t\t\tfor (const message of preCommands) {\n\t\t\t\tawait this.session.prompt(message.text);\n\t\t\t}\n\n\t\t\t// Send first prompt (starts streaming)\n\t\t\tconst promptPromise = this.session.prompt(firstPrompt.text).catch((error) => {\n\t\t\t\trestoreQueue(error);\n\t\t\t});\n\n\t\t\t// Queue remaining messages\n\t\t\tfor (const message of rest) {\n\t\t\t\tif (this.isExtensionCommand(message.text)) {\n\t\t\t\t\tawait this.session.prompt(message.text);\n\t\t\t\t} else if (message.mode === \"followUp\") {\n\t\t\t\t\tawait this.session.followUp(message.text);\n\t\t\t\t} else {\n\t\t\t\t\tawait this.session.steer(message.text);\n\t\t\t\t}\n\t\t\t}\n\t\t\tthis.updatePendingMessagesDisplay();\n\t\t\tvoid promptPromise;\n\t\t} catch (error) {\n\t\t\trestoreQueue(error);\n\t\t}\n\t}\n}\n"]}