import { feature } from 'bun:bundle'import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.mjs'import type { Permutations } from 'src/types/utils.js'import { getSessionId } from '../bootstrap/state.js'import type { AppState } from '../state/AppState.js'import type {QueueOperation,QueueOperationMessage,} from '../types/messageQueueTypes.js'import type {EditablePromptInputMode,PromptInputMode,QueuedCommand,QueuePriority,} from '../types/textInputTypes.js'import type { PastedContent } from './config.js'import { extractTextContent } from './messages.js'import { objectGroupBy } from './objectGroupBy.js'import { recordQueueOperation } from './sessionStorage.js'import { createSignal } from './signal.js'export type SetAppState = (f: (prev: AppState) => AppState) => void// ============================================================================// Logging helper// ============================================================================function logOperation(operation: QueueOperation, content?: string): void {const sessionId = getSessionId()const queueOp: QueueOperationMessage = {type: 'queue-operation',operation,timestamp: new Date().toISOString(),sessionId,...(content !== undefined && { content }),}void recordQueueOperation(queueOp)}// ============================================================================// Unified command queue (module-level, independent of React state)//// All commands — user input, task notifications, orphaned permissions — go// through this single queue. React components subscribe via// useSyncExternalStore (subscribeToCommandQueue / getCommandQueueSnapshot).// Non-React code (print.ts streaming loop) reads directly via// getCommandQueue() / getCommandQueueLength().//// Priority determines dequeue order: 'now' > 'next' > 'later'.// Within the same priority, commands are processed FIFO.// ============================================================================const commandQueue: QueuedCommand[] = []/** Frozen snapshot — recreated on every mutation for useSyncExternalStore. */let snapshot: readonly QueuedCommand[] = Object.freeze([])const queueChanged = createSignal()function notifySubscribers(): void {snapshot = Object.freeze([...commandQueue])queueChanged.emit()}// ============================================================================// useSyncExternalStore interface// ============================================================================/*** Subscribe to command queue changes.* Compatible with React's useSyncExternalStore.*/export const subscribeToCommandQueue = queueChanged.subscribe/*** Get current snapshot of the command queue.* Compatible with React's useSyncExternalStore.* Returns a frozen array that only changes reference on mutation.*/export function getCommandQueueSnapshot(): readonly QueuedCommand[] {return snapshot}// ============================================================================// Read operations (for non-React code)// ============================================================================/*** Get a mutable copy of the current queue.* Use for one-off reads where you need the actual commands.*/export function getCommandQueue(): QueuedCommand[] {return [...commandQueue]}/*** Get the current queue length without copying.*/export function getCommandQueueLength(): number {return commandQueue.length}/*** Check if there are commands in the queue.*/export function hasCommandsInQueue(): boolean {return commandQueue.length > 0}/*** Trigger a re-check by notifying subscribers.* Use after async processing completes to ensure remaining commands* are picked up by useSyncExternalStore consumers.*/export function recheckCommandQueue(): void {if (commandQueue.length > 0) {notifySubscribers()}}// ============================================================================// Write operations// ============================================================================/*** Add a command to the queue.* Used for user-initiated commands (prompt, bash, orphaned-permission).* Defaults priority to 'next' (processed before task notifications).*/export function enqueue(command: QueuedCommand): void {commandQueue.push({ ...command, priority: command.priority ?? 'next' })notifySubscribers()logOperation('enqueue',typeof command.value === 'string' ? command.value : undefined,)}/*** Add a task notification to the queue.* Convenience wrapper that defaults priority to 'later' so user input* is never starved by system messages.*/export function enqueuePendingNotification(command: QueuedCommand): void {commandQueue.push({ ...command, priority: command.priority ?? 'later' })notifySubscribers()logOperation('enqueue',typeof command.value === 'string' ? command.value : undefined,)}const PRIORITY_ORDER: Record<QueuePriority, number> = {now: 0,next: 1,later: 2,}/*** Remove and return the highest-priority command, or undefined if empty.* Within the same priority level, commands are dequeued FIFO.** An optional `filter` narrows the candidates: only commands for which the* predicate returns `true` are considered. Non-matching commands stay in the* queue untouched. This lets between-turn drains (SDK, REPL) restrict to* main-thread commands (`cmd.agentId === undefined`) without restructuring* the existing while-loop patterns.*/export function dequeue(filter?: (cmd: QueuedCommand) => boolean,): QueuedCommand | undefined {if (commandQueue.length === 0) {return undefined}// Find the first command with the highest priority (respecting filter)let bestIdx = -1let bestPriority = Infinityfor (let i = 0; i < commandQueue.length; i++) {const cmd = commandQueue[i]!if (filter && !filter(cmd)) continueconst priority = PRIORITY_ORDER[cmd.priority ?? 'next']if (priority < bestPriority) {bestIdx = ibestPriority = priority}}if (bestIdx === -1) return undefinedconst [dequeued] = commandQueue.splice(bestIdx, 1)notifySubscribers()logOperation('dequeue')return dequeued}/*** Remove and return all commands from the queue.* Logs a dequeue operation for each command.*/export function dequeueAll(): QueuedCommand[] {if (commandQueue.length === 0) {return []}const commands = [...commandQueue]commandQueue.length = 0notifySubscribers()for (const _cmd of commands) {logOperation('dequeue')}return commands}/*** Return the highest-priority command without removing it, or undefined if empty.* Accepts an optional `filter` — only commands passing the predicate are considered.*/export function peek(filter?: (cmd: QueuedCommand) => boolean,): QueuedCommand | undefined {if (commandQueue.length === 0) {return undefined}let bestIdx = -1let bestPriority = Infinityfor (let i = 0; i < commandQueue.length; i++) {const cmd = commandQueue[i]!if (filter && !filter(cmd)) continueconst priority = PRIORITY_ORDER[cmd.priority ?? 'next']if (priority < bestPriority) {bestIdx = ibestPriority = priority}}if (bestIdx === -1) return undefinedreturn commandQueue[bestIdx]}/*** Remove and return all commands matching a predicate, preserving priority order.* Non-matching commands stay in the queue.*/export function dequeueAllMatching(predicate: (cmd: QueuedCommand) => boolean,): QueuedCommand[] {const matched: QueuedCommand[] = []const remaining: QueuedCommand[] = []for (const cmd of commandQueue) {if (predicate(cmd)) {matched.push(cmd)} else {remaining.push(cmd)}}if (matched.length === 0) {return []}commandQueue.length = 0commandQueue.push(...remaining)notifySubscribers()for (const _cmd of matched) {logOperation('dequeue')}return matched}/*** Remove specific commands from the queue by reference identity.* Callers must pass the same object references that are in the queue* (e.g. from getCommandsByMaxPriority). Logs a 'remove' operation for each.*/export function remove(commandsToRemove: QueuedCommand[]): void {if (commandsToRemove.length === 0) {return}const before = commandQueue.lengthfor (let i = commandQueue.length - 1; i >= 0; i--) {if (commandsToRemove.includes(commandQueue[i]!)) {commandQueue.splice(i, 1)}}if (commandQueue.length !== before) {notifySubscribers()}for (const _cmd of commandsToRemove) {logOperation('remove')}}/*** Remove commands matching a predicate.* Returns the removed commands.*/export function removeByFilter(predicate: (cmd: QueuedCommand) => boolean,): QueuedCommand[] {const removed: QueuedCommand[] = []for (let i = commandQueue.length - 1; i >= 0; i--) {if (predicate(commandQueue[i]!)) {removed.unshift(commandQueue.splice(i, 1)[0]!)}}if (removed.length > 0) {notifySubscribers()for (const _cmd of removed) {logOperation('remove')}}return removed}/*** Clear all commands from the queue.* Used by ESC cancellation to discard queued notifications.*/export function clearCommandQueue(): void {if (commandQueue.length === 0) {return}commandQueue.length = 0notifySubscribers()}/*** Clear all commands and reset snapshot.* Used for test cleanup.*/export function resetCommandQueue(): void {commandQueue.length = 0snapshot = Object.freeze([])}// ============================================================================// Editable mode helpers// ============================================================================const NON_EDITABLE_MODES = new Set<PromptInputMode>(['task-notification',] satisfies Permutations<Exclude<PromptInputMode, EditablePromptInputMode>>)export function isPromptInputModeEditable(mode: PromptInputMode,): mode is EditablePromptInputMode {return !NON_EDITABLE_MODES.has(mode)}/*** Whether this queued command can be pulled into the input buffer via UP/ESC.* System-generated commands (proactive ticks, scheduled tasks, plan* verification, channel messages) contain raw XML and must not leak into* the user's input.*/export function isQueuedCommandEditable(cmd: QueuedCommand): boolean {return isPromptInputModeEditable(cmd.mode) && !cmd.isMeta}/*** Whether this queued command should render in the queue preview under the* prompt. Superset of editable — channel messages show (so the keyboard user* sees what arrived) but stay non-editable (raw XML).*/export function isQueuedCommandVisible(cmd: QueuedCommand): boolean {if ((feature('KAIROS') || feature('KAIROS_CHANNELS')) &&cmd.origin?.kind === 'channel')return truereturn isQueuedCommandEditable(cmd)}/*** Extract text from a queued command value.* For strings, returns the string.* For ContentBlockParam[], extracts text from text blocks.*/function extractTextFromValue(value: string | ContentBlockParam[]): string {return typeof value === 'string' ? value : extractTextContent(value, '\n')}/*** Extract images from ContentBlockParam[] and convert to PastedContent format.* Returns empty array for string values or if no images found.*/function extractImagesFromValue(value: string | ContentBlockParam[],startId: number,): PastedContent[] {if (typeof value === 'string') {return []}const images: PastedContent[] = []let imageIndex = 0for (const block of value) {if (block.type === 'image' && block.source.type === 'base64') {images.push({id: startId + imageIndex,type: 'image',content: block.source.data,mediaType: block.source.media_type,filename: `image${imageIndex + 1}`,})imageIndex++}}return images}export type PopAllEditableResult = {text: stringcursorOffset: numberimages: PastedContent[]}/*** Pop all editable commands and combine them with current input for editing.* Notification modes (task-notification) are left in the queue* to be auto-processed later.* Returns object with combined text, cursor offset, and images to restore.* Returns undefined if no editable commands in queue.*/export function popAllEditable(currentInput: string,currentCursorOffset: number,): PopAllEditableResult | undefined {if (commandQueue.length === 0) {return undefined}const { editable = [], nonEditable = [] } = objectGroupBy([...commandQueue],cmd => (isQueuedCommandEditable(cmd) ? 'editable' : 'nonEditable'),)if (editable.length === 0) {return undefined}// Extract text from queued commands (handles both strings and ContentBlockParam[])const queuedTexts = editable.map(cmd => extractTextFromValue(cmd.value))const newInput = [...queuedTexts, currentInput].filter(Boolean).join('\n')// Calculate cursor offset: length of joined queued commands + 1 + current cursor offsetconst cursorOffset = queuedTexts.join('\n').length + 1 + currentCursorOffset// Extract images from queued commandsconst images: PastedContent[] = []let nextImageId = Date.now() // Use timestamp as base for unique IDsfor (const cmd of editable) {// handlePromptSubmit queues images in pastedContents (value is a string).// Preserve the original PastedContent id so imageStore lookups still work.if (cmd.pastedContents) {for (const content of Object.values(cmd.pastedContents)) {if (content.type === 'image') {images.push(content)}}}// Bridge/remote commands may embed images directly in ContentBlockParam[].const cmdImages = extractImagesFromValue(cmd.value, nextImageId)images.push(...cmdImages)nextImageId += cmdImages.length}for (const command of editable) {logOperation('popAll',typeof command.value === 'string' ? command.value : undefined,)}// Replace queue contents with only the non-editable commandscommandQueue.length = 0commandQueue.push(...nonEditable)notifySubscribers()return { text: newInput, cursorOffset, images }}// ============================================================================// Backward-compatible aliases (deprecated — prefer new names)// ============================================================================/** @deprecated Use subscribeToCommandQueue */export const subscribeToPendingNotifications = subscribeToCommandQueue/** @deprecated Use getCommandQueueSnapshot */export function getPendingNotificationsSnapshot(): readonly QueuedCommand[] {return snapshot}/** @deprecated Use hasCommandsInQueue */export const hasPendingNotifications = hasCommandsInQueue/** @deprecated Use getCommandQueueLength */export const getPendingNotificationsCount = getCommandQueueLength/** @deprecated Use recheckCommandQueue */export const recheckPendingNotifications = recheckCommandQueue/** @deprecated Use dequeue */export function dequeuePendingNotification(): QueuedCommand | undefined {return dequeue()}/** @deprecated Use resetCommandQueue */export const resetPendingNotifications = resetCommandQueue/** @deprecated Use clearCommandQueue */export const clearPendingNotifications = clearCommandQueue/*** Get commands at or above a given priority level without removing them.* Useful for mid-chain draining where only urgent items should be processed.** Priority order: 'now' (0) > 'next' (1) > 'later' (2).* Passing 'now' returns only now-priority commands; 'later' returns everything.*/export function getCommandsByMaxPriority(maxPriority: QueuePriority,): QueuedCommand[] {const threshold = PRIORITY_ORDER[maxPriority]return commandQueue.filter(cmd => PRIORITY_ORDER[cmd.priority ?? 'next'] <= threshold,)}/*** Returns true if the command is a slash command that should be routed through* processSlashCommand rather than sent to the model as text.** Commands with `skipSlashCommands` (e.g. bridge/CCR messages) are NOT treated* as slash commands — their text is meant for the model.*/export function isSlashCommand(cmd: QueuedCommand): boolean {return (typeof cmd.value === 'string' &&cmd.value.trim().startsWith('/') &&!cmd.skipSlashCommands)}
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。