) => {
+ event.preventDefault()
+ event.stopPropagation()
+ dragControls.start(event)
+ },
+ [dragControls]
+ )
+
+ return (
+
+
+
+ #{index + 1}
+
+
+ {item.draft.displayText}
+
+
+
+
+ )
+}
+
+export function MessageQueueDisplay({
+ queue,
+ onReorder,
+ onEdit,
+ onDelete,
+ editingItemId,
+}: MessageQueueDisplayProps) {
+ if (queue.length === 0) return null
+
+ return (
+
+
+ {queue.map((item, index) => (
+
+ ))}
+
+
+ )
+}
diff --git a/src/components/conversations/conversation-detail-panel.tsx b/src/components/conversations/conversation-detail-panel.tsx
index 61eabcc..cea7a4f 100644
--- a/src/components/conversations/conversation-detail-panel.tsx
+++ b/src/components/conversations/conversation-detail-panel.tsx
@@ -18,6 +18,7 @@ import { useTabContext } from "@/contexts/tab-context"
import { useSessionStats } from "@/contexts/session-stats-context"
import { cn } from "@/lib/utils"
import { useConnectionLifecycle } from "@/hooks/use-connection-lifecycle"
+import { useMessageQueue, type QueuedMessage } from "@/hooks/use-message-queue"
import { MessageListView } from "@/components/message/message-list-view"
import { ConversationShell } from "@/components/chat/conversation-shell"
import { AgentSelector } from "@/components/chat/agent-selector"
@@ -254,6 +255,18 @@ const ConversationTabView = memo(function ConversationTabView({
disconnect: connDisconnect,
sessionId: connSessionId,
} = conn
+ const messageQueue = useMessageQueue()
+ const {
+ queue: msgQueue,
+ enqueue: mqEnqueue,
+ dequeue: mqDequeue,
+ remove: mqRemove,
+ reorder: mqReorder,
+ updateItem: mqUpdateItem,
+ editingItemId: mqEditingItemId,
+ startEditing: mqStartEditing,
+ cancelEditing: mqCancelEditing,
+ } = messageQueue
const connStatusRef = useRef(connStatus)
useEffect(() => {
connStatusRef.current = connStatus
@@ -329,6 +342,32 @@ const ConversationTabView = memo(function ConversationTabView({
syncTurnMetadata,
])
+ // Auto-send queued messages when agent finishes responding.
+ // Refs are synced via useEffect; the auto-send effect is declared
+ // AFTER completeTurn so React runs it second.
+ const autoSendQueueRef = useRef<() => QueuedMessage | undefined>(mqDequeue)
+ useEffect(() => {
+ autoSendQueueRef.current = mqDequeue
+ }, [mqDequeue])
+ const handleSendRef = useRef<
+ (draft: PromptDraft, modeId?: string | null) => void
+ >(() => {})
+
+ const prevAutoSendStatusRef = useRef(connStatus)
+ useEffect(() => {
+ const wasPrompting = prevAutoSendStatusRef.current === "prompting"
+ prevAutoSendStatusRef.current = connStatus
+ if (!wasPrompting || connStatus !== "connected") return
+
+ // Use queueMicrotask to ensure completeTurn effect has fully committed
+ queueMicrotask(() => {
+ const next = autoSendQueueRef.current()
+ if (next) {
+ handleSendRef.current(next.draft, next.modeId)
+ }
+ })
+ }, [connStatus])
+
useEffect(() => {
// Only sync non-null liveMessage updates to state. When conn.liveMessage
// goes null (agent finished streaming), don't clear state.liveMessage —
@@ -545,6 +584,11 @@ const ConversationTabView = memo(function ConversationTabView({
]
)
+ // Sync handleSend ref for auto-send effect (declared before handleSend)
+ useEffect(() => {
+ handleSendRef.current = handleSend
+ }, [handleSend])
+
const handleOpenAgentsSettings = useCallback(() => {
openSettingsWindow("agents", { agentType: selectedAgent }).catch((err) => {
console.error(
@@ -615,6 +659,33 @@ const ConversationTabView = memo(function ConversationTabView({
]
)
+ // Queue edit flow: derive editing draft text from queue state
+ const editingQueueDraftText = useMemo(() => {
+ if (!mqEditingItemId) return null
+ const item = msgQueue.find((m) => m.id === mqEditingItemId)
+ return item?.draft.displayText ?? null
+ }, [mqEditingItemId, msgQueue])
+
+ const handleQueueEdit = useCallback(
+ (id: string) => {
+ mqStartEditing(id)
+ },
+ [mqStartEditing]
+ )
+
+ const handleQueueCancelEdit = useCallback(() => {
+ mqCancelEditing()
+ }, [mqCancelEditing])
+
+ const handleSaveQueueEdit = useCallback(
+ (draft: PromptDraft) => {
+ if (mqEditingItemId) {
+ mqUpdateItem(mqEditingItemId, draft)
+ }
+ },
+ [mqEditingItemId, mqUpdateItem]
+ )
+
const showDraftHeader = !hasPersistedConversation && !hasSentMessage
const isWelcomeMode = showDraftHeader
@@ -657,6 +728,16 @@ const ConversationTabView = memo(function ConversationTabView({
draftStorageKey={draftStorageKey}
hideInput={isWelcomeMode}
isActive={isActive}
+ queue={msgQueue}
+ onEnqueue={mqEnqueue}
+ onQueueReorder={mqReorder}
+ onQueueEdit={handleQueueEdit}
+ onQueueDelete={mqRemove}
+ editingItemId={mqEditingItemId}
+ editingDraftText={editingQueueDraftText}
+ isEditingQueueItem={mqEditingItemId != null}
+ onSaveQueueEdit={handleSaveQueueEdit}
+ onCancelQueueEdit={handleQueueCancelEdit}
>
{isWelcomeMode ? (
diff --git a/src/hooks/use-message-queue.ts b/src/hooks/use-message-queue.ts
new file mode 100644
index 0000000..948b539
--- /dev/null
+++ b/src/hooks/use-message-queue.ts
@@ -0,0 +1,89 @@
+"use client"
+
+import { useCallback, useEffect, useRef, useState } from "react"
+import type { PromptDraft } from "@/lib/types"
+
+export interface QueuedMessage {
+ id: string
+ draft: PromptDraft
+ modeId: string | null
+}
+
+export interface UseMessageQueueReturn {
+ queue: QueuedMessage[]
+ enqueue: (draft: PromptDraft, modeId: string | null) => void
+ dequeue: () => QueuedMessage | undefined
+ remove: (id: string) => void
+ reorder: (items: QueuedMessage[]) => void
+ updateItem: (id: string, draft: PromptDraft) => void
+ editingItemId: string | null
+ startEditing: (id: string) => void
+ cancelEditing: () => void
+}
+
+export function useMessageQueue(): UseMessageQueueReturn {
+ const [queue, setQueue] = useState([])
+ const [editingItemId, setEditingItemId] = useState(null)
+ const queueRef = useRef(queue)
+ useEffect(() => {
+ queueRef.current = queue
+ }, [queue])
+
+ const enqueue = useCallback((draft: PromptDraft, modeId: string | null) => {
+ const item: QueuedMessage = {
+ id: crypto.randomUUID(),
+ draft,
+ modeId,
+ }
+ setQueue((prev) => [...prev, item])
+ }, [])
+
+ const dequeue = useCallback((): QueuedMessage | undefined => {
+ const current = queueRef.current
+ if (current.length === 0) return undefined
+ const first = current[0]
+ setQueue((prev) => prev.slice(1))
+ return first
+ }, [])
+
+ const remove = useCallback(
+ (id: string) => {
+ if (editingItemId === id) {
+ setEditingItemId(null)
+ }
+ setQueue((prev) => prev.filter((item) => item.id !== id))
+ },
+ [editingItemId]
+ )
+
+ const reorder = useCallback((items: QueuedMessage[]) => {
+ setQueue(items)
+ }, [])
+
+ const updateItem = useCallback((id: string, draft: PromptDraft) => {
+ setQueue((prev) =>
+ prev.map((item) => (item.id === id ? { ...item, draft } : item))
+ )
+ setEditingItemId(null)
+ }, [])
+
+ const startEditing = useCallback((id: string) => {
+ setEditingItemId(id)
+ }, [])
+
+ const cancelEditing = useCallback(() => {
+ setEditingItemId(null)
+ }, [])
+
+ return {
+ queue,
+ enqueue,
+ dequeue,
+ remove,
+ reorder,
+ updateItem,
+ editingItemId,
+ startEditing,
+ cancelEditing,
+ }
+}
diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json
index 9729a07..fb651cb 100644
--- a/src/i18n/messages/ar.json
+++ b/src/i18n/messages/ar.json
@@ -1145,6 +1145,13 @@
"cancel": "إلغاء",
"send": "إرسال"
},
+ "messageQueue": {
+ "addToQueue": "إضافة للقائمة",
+ "saveEdit": "حفظ",
+ "cancelEdit": "إلغاء التعديل",
+ "editItem": "تعديل",
+ "deleteItem": "حذف"
+ },
"welcomeInputPanel": {
"agentsSettingsPath": "الإعدادات > الوكلاء",
"autoConnectFallback": "انقر لفتح {path} وإدارة التثبيت.",
diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json
index 52bd16c..4402d85 100644
--- a/src/i18n/messages/de.json
+++ b/src/i18n/messages/de.json
@@ -1145,6 +1145,13 @@
"cancel": "Abbrechen",
"send": "Senden"
},
+ "messageQueue": {
+ "addToQueue": "Zur Warteschlange",
+ "saveEdit": "Speichern",
+ "cancelEdit": "Bearbeitung abbrechen",
+ "editItem": "Bearbeiten",
+ "deleteItem": "Entfernen"
+ },
"welcomeInputPanel": {
"agentsSettingsPath": "Einstellungen > Agenten",
"autoConnectFallback": "Klicken Sie, um {path} zu öffnen und die Installation zu verwalten.",
diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json
index 43d959f..f1e80c1 100644
--- a/src/i18n/messages/en.json
+++ b/src/i18n/messages/en.json
@@ -1145,6 +1145,13 @@
"cancel": "Cancel",
"send": "Send"
},
+ "messageQueue": {
+ "addToQueue": "Queue message",
+ "saveEdit": "Save",
+ "cancelEdit": "Cancel edit",
+ "editItem": "Edit",
+ "deleteItem": "Remove"
+ },
"welcomeInputPanel": {
"agentsSettingsPath": "Settings > Agents",
"autoConnectFallback": "Click to open {path} and manage installation.",
diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json
index f86127d..96380f9 100644
--- a/src/i18n/messages/es.json
+++ b/src/i18n/messages/es.json
@@ -1145,6 +1145,13 @@
"cancel": "Cancelar",
"send": "Enviar"
},
+ "messageQueue": {
+ "addToQueue": "Agregar a la cola",
+ "saveEdit": "Guardar",
+ "cancelEdit": "Cancelar edición",
+ "editItem": "Editar",
+ "deleteItem": "Eliminar"
+ },
"welcomeInputPanel": {
"agentsSettingsPath": "Ajustes > Agentes",
"autoConnectFallback": "Haz clic para abrir {path} y gestionar la instalación.",
diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json
index 30e856a..8188c08 100644
--- a/src/i18n/messages/fr.json
+++ b/src/i18n/messages/fr.json
@@ -1145,6 +1145,13 @@
"cancel": "Annuler",
"send": "Envoyer"
},
+ "messageQueue": {
+ "addToQueue": "Mettre en file",
+ "saveEdit": "Enregistrer",
+ "cancelEdit": "Annuler la modification",
+ "editItem": "Modifier",
+ "deleteItem": "Supprimer"
+ },
"welcomeInputPanel": {
"agentsSettingsPath": "Paramètres > Agents",
"autoConnectFallback": "Cliquez pour ouvrir {path} et gérer l'installation.",
diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json
index d41b1dc..211572c 100644
--- a/src/i18n/messages/ja.json
+++ b/src/i18n/messages/ja.json
@@ -1145,6 +1145,13 @@
"cancel": "キャンセル",
"send": "送信"
},
+ "messageQueue": {
+ "addToQueue": "キューに追加",
+ "saveEdit": "保存",
+ "cancelEdit": "編集をキャンセル",
+ "editItem": "編集",
+ "deleteItem": "削除"
+ },
"welcomeInputPanel": {
"agentsSettingsPath": "設定 > エージェント",
"autoConnectFallback": "{path} を開いてインストールを管理してください。",
diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json
index bb8e6b8..b58fa62 100644
--- a/src/i18n/messages/ko.json
+++ b/src/i18n/messages/ko.json
@@ -1145,6 +1145,13 @@
"cancel": "취소",
"send": "보내기"
},
+ "messageQueue": {
+ "addToQueue": "대기열에 추가",
+ "saveEdit": "저장",
+ "cancelEdit": "편집 취소",
+ "editItem": "편집",
+ "deleteItem": "삭제"
+ },
"welcomeInputPanel": {
"agentsSettingsPath": "설정 > 에이전트",
"autoConnectFallback": "{path}을(를) 열어 설치를 관리하세요.",
diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json
index 0db30af..1ce9763 100644
--- a/src/i18n/messages/pt.json
+++ b/src/i18n/messages/pt.json
@@ -1145,6 +1145,13 @@
"cancel": "Cancelar",
"send": "Enviar"
},
+ "messageQueue": {
+ "addToQueue": "Adicionar à fila",
+ "saveEdit": "Salvar",
+ "cancelEdit": "Cancelar edição",
+ "editItem": "Editar",
+ "deleteItem": "Remover"
+ },
"welcomeInputPanel": {
"agentsSettingsPath": "Configurações > Agentes",
"autoConnectFallback": "Clique para abrir {path} e gerenciar a instalação.",
diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json
index aa3cb30..88afb2c 100644
--- a/src/i18n/messages/zh-CN.json
+++ b/src/i18n/messages/zh-CN.json
@@ -1145,6 +1145,13 @@
"cancel": "取消",
"send": "发送"
},
+ "messageQueue": {
+ "addToQueue": "加入队列",
+ "saveEdit": "保存",
+ "cancelEdit": "取消编辑",
+ "editItem": "编辑",
+ "deleteItem": "删除"
+ },
"welcomeInputPanel": {
"agentsSettingsPath": "设置 > Agents",
"autoConnectFallback": "点击前往 {path} 管理安装。",
diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json
index de959ac..f29dcb6 100644
--- a/src/i18n/messages/zh-TW.json
+++ b/src/i18n/messages/zh-TW.json
@@ -1145,6 +1145,13 @@
"cancel": "取消",
"send": "傳送"
},
+ "messageQueue": {
+ "addToQueue": "加入佇列",
+ "saveEdit": "儲存",
+ "cancelEdit": "取消編輯",
+ "editItem": "編輯",
+ "deleteItem": "刪除"
+ },
"welcomeInputPanel": {
"agentsSettingsPath": "設定 > Agents",
"autoConnectFallback": "點擊前往 {path} 管理安裝。",