支持会话响应时添加队列消息
This commit is contained in:
@@ -9,7 +9,9 @@ import type {
|
|||||||
SessionModeInfo,
|
SessionModeInfo,
|
||||||
AvailableCommandInfo,
|
AvailableCommandInfo,
|
||||||
} from "@/lib/types"
|
} from "@/lib/types"
|
||||||
|
import type { QueuedMessage } from "@/hooks/use-message-queue"
|
||||||
import { MessageInput } from "@/components/chat/message-input"
|
import { MessageInput } from "@/components/chat/message-input"
|
||||||
|
import { MessageQueueDisplay } from "@/components/chat/message-queue-display"
|
||||||
|
|
||||||
interface ChatInputProps {
|
interface ChatInputProps {
|
||||||
status: ConnectionStatus | null
|
status: ConnectionStatus | null
|
||||||
@@ -29,6 +31,16 @@ interface ChatInputProps {
|
|||||||
attachmentTabId?: string | null
|
attachmentTabId?: string | null
|
||||||
draftStorageKey?: string | null
|
draftStorageKey?: string | null
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
|
queue?: QueuedMessage[]
|
||||||
|
onEnqueue?: (draft: PromptDraft, modeId: string | null) => void
|
||||||
|
onQueueReorder?: (items: QueuedMessage[]) => void
|
||||||
|
onQueueEdit?: (id: string) => void
|
||||||
|
onQueueDelete?: (id: string) => void
|
||||||
|
editingItemId?: string | null
|
||||||
|
editingDraftText?: string | null
|
||||||
|
isEditingQueueItem?: boolean
|
||||||
|
onSaveQueueEdit?: (draft: PromptDraft) => void
|
||||||
|
onCancelQueueEdit?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ChatInput({
|
export function ChatInput({
|
||||||
@@ -49,6 +61,16 @@ export function ChatInput({
|
|||||||
attachmentTabId,
|
attachmentTabId,
|
||||||
draftStorageKey,
|
draftStorageKey,
|
||||||
isActive,
|
isActive,
|
||||||
|
queue,
|
||||||
|
onEnqueue,
|
||||||
|
onQueueReorder,
|
||||||
|
onQueueEdit,
|
||||||
|
onQueueDelete,
|
||||||
|
editingItemId,
|
||||||
|
editingDraftText,
|
||||||
|
isEditingQueueItem,
|
||||||
|
onSaveQueueEdit,
|
||||||
|
onCancelQueueEdit,
|
||||||
}: ChatInputProps) {
|
}: ChatInputProps) {
|
||||||
const t = useTranslations("Folder.chat.chatInput")
|
const t = useTranslations("Folder.chat.chatInput")
|
||||||
const isConnected = status === "connected"
|
const isConnected = status === "connected"
|
||||||
@@ -57,12 +79,25 @@ export function ChatInput({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 pt-0">
|
<div className="p-4 pt-0">
|
||||||
|
{queue &&
|
||||||
|
queue.length > 0 &&
|
||||||
|
onQueueReorder &&
|
||||||
|
onQueueEdit &&
|
||||||
|
onQueueDelete && (
|
||||||
|
<MessageQueueDisplay
|
||||||
|
queue={queue}
|
||||||
|
onReorder={onQueueReorder}
|
||||||
|
onEdit={onQueueEdit}
|
||||||
|
onDelete={onQueueDelete}
|
||||||
|
editingItemId={editingItemId ?? null}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<MessageInput
|
<MessageInput
|
||||||
onSend={onSend}
|
onSend={onSend}
|
||||||
promptCapabilities={promptCapabilities}
|
promptCapabilities={promptCapabilities}
|
||||||
onFocus={onFocus}
|
onFocus={onFocus}
|
||||||
defaultPath={defaultPath}
|
defaultPath={defaultPath}
|
||||||
disabled={!isConnected}
|
disabled={!isConnected && !isPrompting}
|
||||||
isPrompting={isPrompting}
|
isPrompting={isPrompting}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
modes={modes}
|
modes={modes}
|
||||||
@@ -76,6 +111,11 @@ export function ChatInput({
|
|||||||
attachmentTabId={attachmentTabId}
|
attachmentTabId={attachmentTabId}
|
||||||
draftStorageKey={draftStorageKey}
|
draftStorageKey={draftStorageKey}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
|
onEnqueue={onEnqueue}
|
||||||
|
editingDraftText={editingDraftText}
|
||||||
|
isEditingQueueItem={isEditingQueueItem}
|
||||||
|
onSaveQueueEdit={onSaveQueueEdit}
|
||||||
|
onCancelQueueEdit={onCancelQueueEdit}
|
||||||
placeholder={
|
placeholder={
|
||||||
isConnecting
|
isConnecting
|
||||||
? t("connecting")
|
? t("connecting")
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
PendingPermission,
|
PendingPermission,
|
||||||
PendingQuestion,
|
PendingQuestion,
|
||||||
} from "@/contexts/acp-connections-context"
|
} from "@/contexts/acp-connections-context"
|
||||||
|
import type { QueuedMessage } from "@/hooks/use-message-queue"
|
||||||
import { ChatInput } from "@/components/chat/chat-input"
|
import { ChatInput } from "@/components/chat/chat-input"
|
||||||
import { PermissionDialog } from "@/components/chat/permission-dialog"
|
import { PermissionDialog } from "@/components/chat/permission-dialog"
|
||||||
import { QuestionDialog } from "@/components/chat/question-dialog"
|
import { QuestionDialog } from "@/components/chat/question-dialog"
|
||||||
@@ -40,6 +41,16 @@ interface ConversationShellProps {
|
|||||||
draftStorageKey?: string | null
|
draftStorageKey?: string | null
|
||||||
hideInput?: boolean
|
hideInput?: boolean
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
|
queue?: QueuedMessage[]
|
||||||
|
onEnqueue?: (draft: PromptDraft, modeId: string | null) => void
|
||||||
|
onQueueReorder?: (items: QueuedMessage[]) => void
|
||||||
|
onQueueEdit?: (id: string) => void
|
||||||
|
onQueueDelete?: (id: string) => void
|
||||||
|
editingItemId?: string | null
|
||||||
|
editingDraftText?: string | null
|
||||||
|
isEditingQueueItem?: boolean
|
||||||
|
onSaveQueueEdit?: (draft: PromptDraft) => void
|
||||||
|
onCancelQueueEdit?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ConversationShell({
|
export function ConversationShell({
|
||||||
@@ -67,6 +78,16 @@ export function ConversationShell({
|
|||||||
draftStorageKey,
|
draftStorageKey,
|
||||||
hideInput = false,
|
hideInput = false,
|
||||||
isActive,
|
isActive,
|
||||||
|
queue,
|
||||||
|
onEnqueue,
|
||||||
|
onQueueReorder,
|
||||||
|
onQueueEdit,
|
||||||
|
onQueueDelete,
|
||||||
|
editingItemId,
|
||||||
|
editingDraftText,
|
||||||
|
isEditingQueueItem,
|
||||||
|
onSaveQueueEdit,
|
||||||
|
onCancelQueueEdit,
|
||||||
}: ConversationShellProps) {
|
}: ConversationShellProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full min-h-0 flex-col">
|
<div className="flex h-full min-h-0 flex-col">
|
||||||
@@ -98,6 +119,16 @@ export function ConversationShell({
|
|||||||
attachmentTabId={attachmentTabId}
|
attachmentTabId={attachmentTabId}
|
||||||
draftStorageKey={draftStorageKey}
|
draftStorageKey={draftStorageKey}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
|
queue={queue}
|
||||||
|
onEnqueue={onEnqueue}
|
||||||
|
onQueueReorder={onQueueReorder}
|
||||||
|
onQueueEdit={onQueueEdit}
|
||||||
|
onQueueDelete={onQueueDelete}
|
||||||
|
editingItemId={editingItemId}
|
||||||
|
editingDraftText={editingDraftText}
|
||||||
|
isEditingQueueItem={isEditingQueueItem}
|
||||||
|
onSaveQueueEdit={onSaveQueueEdit}
|
||||||
|
onCancelQueueEdit={onCancelQueueEdit}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,16 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/components/ui/popover"
|
} from "@/components/ui/popover"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { Ellipsis, FileSearch, Plus, Send, Square, X } from "lucide-react"
|
import {
|
||||||
|
Check,
|
||||||
|
Ellipsis,
|
||||||
|
FileSearch,
|
||||||
|
ListPlus,
|
||||||
|
Plus,
|
||||||
|
Send,
|
||||||
|
Square,
|
||||||
|
X,
|
||||||
|
} from "lucide-react"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { matchShortcutEvent } from "@/lib/keyboard-shortcuts"
|
import { matchShortcutEvent } from "@/lib/keyboard-shortcuts"
|
||||||
import { useShortcutSettings } from "@/hooks/use-shortcut-settings"
|
import { useShortcutSettings } from "@/hooks/use-shortcut-settings"
|
||||||
@@ -62,6 +71,11 @@ interface MessageInputProps {
|
|||||||
attachmentTabId?: string | null
|
attachmentTabId?: string | null
|
||||||
draftStorageKey?: string | null
|
draftStorageKey?: string | null
|
||||||
isActive?: boolean
|
isActive?: boolean
|
||||||
|
onEnqueue?: (draft: PromptDraft, modeId: string | null) => void
|
||||||
|
editingDraftText?: string | null
|
||||||
|
isEditingQueueItem?: boolean
|
||||||
|
onSaveQueueEdit?: (draft: PromptDraft) => void
|
||||||
|
onCancelQueueEdit?: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ResourceInputAttachment {
|
interface ResourceInputAttachment {
|
||||||
@@ -261,8 +275,14 @@ export function MessageInput({
|
|||||||
attachmentTabId,
|
attachmentTabId,
|
||||||
draftStorageKey,
|
draftStorageKey,
|
||||||
isActive = false,
|
isActive = false,
|
||||||
|
onEnqueue,
|
||||||
|
editingDraftText,
|
||||||
|
isEditingQueueItem = false,
|
||||||
|
onSaveQueueEdit,
|
||||||
|
onCancelQueueEdit,
|
||||||
}: MessageInputProps) {
|
}: MessageInputProps) {
|
||||||
const t = useTranslations("Folder.chat.messageInput")
|
const t = useTranslations("Folder.chat.messageInput")
|
||||||
|
const tQueue = useTranslations("Folder.chat.messageQueue")
|
||||||
const { shortcuts } = useShortcutSettings()
|
const { shortcuts } = useShortcutSettings()
|
||||||
const effectiveDraftStorageKey = draftStorageKey ?? attachmentTabId ?? null
|
const effectiveDraftStorageKey = draftStorageKey ?? attachmentTabId ?? null
|
||||||
const resolvedPlaceholder = placeholder ?? t("askAnything")
|
const resolvedPlaceholder = placeholder ?? t("askAnything")
|
||||||
@@ -302,6 +322,24 @@ export function MessageInput({
|
|||||||
isPromptingRef.current = isPrompting
|
isPromptingRef.current = isPrompting
|
||||||
}, [isPrompting])
|
}, [isPrompting])
|
||||||
|
|
||||||
|
// Load external draft text when editing a queue item
|
||||||
|
const prevEditingDraftRef = useRef<string | null>(null)
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
isEditingQueueItem &&
|
||||||
|
editingDraftText != null &&
|
||||||
|
editingDraftText !== prevEditingDraftRef.current
|
||||||
|
) {
|
||||||
|
prevEditingDraftRef.current = editingDraftText
|
||||||
|
setText(editingDraftText)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
textareaRef.current?.focus()
|
||||||
|
})
|
||||||
|
} else if (!isEditingQueueItem) {
|
||||||
|
prevEditingDraftRef.current = null
|
||||||
|
}
|
||||||
|
}, [isEditingQueueItem, editingDraftText])
|
||||||
|
|
||||||
const setDragActiveIfChanged = useCallback((next: boolean) => {
|
const setDragActiveIfChanged = useCallback((next: boolean) => {
|
||||||
if (dragActiveRef.current === next) return
|
if (dragActiveRef.current === next) return
|
||||||
dragActiveRef.current = next
|
dragActiveRef.current = next
|
||||||
@@ -309,9 +347,9 @@ export function MessageInput({
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!effectiveDraftStorageKey) return
|
if (!effectiveDraftStorageKey || isEditingQueueItem) return
|
||||||
saveMessageInputDraft(effectiveDraftStorageKey, text)
|
saveMessageInputDraft(effectiveDraftStorageKey, text)
|
||||||
}, [effectiveDraftStorageKey, text])
|
}, [effectiveDraftStorageKey, text, isEditingQueueItem])
|
||||||
|
|
||||||
const availableModes = useMemo(() => modes ?? [], [modes])
|
const availableModes = useMemo(() => modes ?? [], [modes])
|
||||||
const availableConfigOptions = useMemo(
|
const availableConfigOptions = useMemo(
|
||||||
@@ -653,7 +691,7 @@ export function MessageInput({
|
|||||||
|
|
||||||
const handlePaste = useCallback(
|
const handlePaste = useCallback(
|
||||||
(event: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
(event: React.ClipboardEvent<HTMLTextAreaElement>) => {
|
||||||
if (disabled || isPrompting) return
|
if (disabled) return
|
||||||
const files = Array.from(event.clipboardData?.files ?? [])
|
const files = Array.from(event.clipboardData?.files ?? [])
|
||||||
if (files.length === 0) return
|
if (files.length === 0) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
@@ -661,7 +699,7 @@ export function MessageInput({
|
|||||||
console.error("[MessageInput] paste files failed:", error)
|
console.error("[MessageInput] paste files failed:", error)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[appendFilesFromInput, disabled, isPrompting]
|
[appendFilesFromInput, disabled]
|
||||||
)
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -763,15 +801,13 @@ export function MessageInput({
|
|||||||
if (payload.type === "drop") {
|
if (payload.type === "drop") {
|
||||||
setDragActiveIfChanged(false)
|
setDragActiveIfChanged(false)
|
||||||
if (Date.now() - lastDomDropAtRef.current < 250) return
|
if (Date.now() - lastDomDropAtRef.current < 250) return
|
||||||
if (!inside || disabledRef.current || isPromptingRef.current) return
|
if (!inside || disabledRef.current) return
|
||||||
void appendPathsFromDropRef.current(payload.paths).catch((error) => {
|
void appendPathsFromDropRef.current(payload.paths).catch((error) => {
|
||||||
console.error("[MessageInput] drag drop paths failed:", error)
|
console.error("[MessageInput] drag drop paths failed:", error)
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setDragActiveIfChanged(
|
setDragActiveIfChanged(inside && !disabledRef.current)
|
||||||
inside && !disabledRef.current && !isPromptingRef.current
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const setup = async () => {
|
const setup = async () => {
|
||||||
@@ -843,9 +879,9 @@ export function MessageInput({
|
|||||||
setAttachments((prev) => prev.filter((item) => item.id !== id))
|
setAttachments((prev) => prev.filter((item) => item.id !== id))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleSend = useCallback(() => {
|
const buildDraft = useCallback((): PromptDraft | null => {
|
||||||
const trimmed = textRef.current.trim()
|
const trimmed = textRef.current.trim()
|
||||||
if (!trimmed && attachments.length === 0) return
|
if (!trimmed && attachments.length === 0) return null
|
||||||
|
|
||||||
const blocks: PromptInputBlock[] = []
|
const blocks: PromptInputBlock[] = []
|
||||||
if (trimmed) {
|
if (trimmed) {
|
||||||
@@ -883,14 +919,41 @@ export function MessageInput({
|
|||||||
const displayText =
|
const displayText =
|
||||||
trimmed ||
|
trimmed ||
|
||||||
`Attached ${attachments.length} attachment${attachments.length > 1 ? "s" : ""}`
|
`Attached ${attachments.length} attachment${attachments.length > 1 ? "s" : ""}`
|
||||||
onSend({ blocks, displayText }, showModeSelector ? effectiveModeId : null)
|
return { blocks, displayText }
|
||||||
|
}, [attachments])
|
||||||
|
|
||||||
|
const handleSend = useCallback(() => {
|
||||||
|
const draft = buildDraft()
|
||||||
|
if (!draft) return
|
||||||
|
|
||||||
|
// Edit mode: save back to queue item
|
||||||
|
if (isEditingQueueItem && onSaveQueueEdit) {
|
||||||
|
onSaveQueueEdit(draft)
|
||||||
|
setText("")
|
||||||
|
setAttachments([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prompting mode: enqueue instead of sending
|
||||||
|
if (isPrompting && onEnqueue) {
|
||||||
|
onEnqueue(draft, showModeSelector ? effectiveModeId : null)
|
||||||
|
setText("")
|
||||||
|
setAttachments([])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
onSend(draft, showModeSelector ? effectiveModeId : null)
|
||||||
if (effectiveDraftStorageKey) {
|
if (effectiveDraftStorageKey) {
|
||||||
clearMessageInputDraft(effectiveDraftStorageKey)
|
clearMessageInputDraft(effectiveDraftStorageKey)
|
||||||
}
|
}
|
||||||
setText("")
|
setText("")
|
||||||
setAttachments([])
|
setAttachments([])
|
||||||
}, [
|
}, [
|
||||||
attachments,
|
buildDraft,
|
||||||
|
isEditingQueueItem,
|
||||||
|
isPrompting,
|
||||||
|
onSaveQueueEdit,
|
||||||
|
onEnqueue,
|
||||||
onSend,
|
onSend,
|
||||||
effectiveModeId,
|
effectiveModeId,
|
||||||
showModeSelector,
|
showModeSelector,
|
||||||
@@ -935,9 +998,15 @@ export function MessageInput({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isEditingQueueItem && e.key === "Escape") {
|
||||||
|
e.preventDefault()
|
||||||
|
onCancelQueueEdit?.()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (matchShortcutEvent(e, shortcuts.send_message)) {
|
if (matchShortcutEvent(e, shortcuts.send_message)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (!disabled) handleSend()
|
if (!disabled || isPrompting || isEditingQueueItem) handleSend()
|
||||||
} else if (matchShortcutEvent(e, shortcuts.newline_in_message)) {
|
} else if (matchShortcutEvent(e, shortcuts.newline_in_message)) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const textarea = e.currentTarget as HTMLTextAreaElement
|
const textarea = e.currentTarget as HTMLTextAreaElement
|
||||||
@@ -953,6 +1022,9 @@ export function MessageInput({
|
|||||||
},
|
},
|
||||||
[
|
[
|
||||||
disabled,
|
disabled,
|
||||||
|
isPrompting,
|
||||||
|
isEditingQueueItem,
|
||||||
|
onCancelQueueEdit,
|
||||||
handleSend,
|
handleSend,
|
||||||
shortcuts,
|
shortcuts,
|
||||||
slashMenuOpen,
|
slashMenuOpen,
|
||||||
@@ -966,11 +1038,11 @@ export function MessageInput({
|
|||||||
(event: React.DragEvent<HTMLDivElement>) => {
|
(event: React.DragEvent<HTMLDivElement>) => {
|
||||||
if (!hasDragFiles(event.dataTransfer)) return
|
if (!hasDragFiles(event.dataTransfer)) return
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
if (!disabled && !isPrompting) {
|
if (!disabled) {
|
||||||
setDragActiveIfChanged(true)
|
setDragActiveIfChanged(true)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[disabled, isPrompting, setDragActiveIfChanged]
|
[disabled, setDragActiveIfChanged]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleContainerDragLeave = useCallback(
|
const handleContainerDragLeave = useCallback(
|
||||||
@@ -994,7 +1066,7 @@ export function MessageInput({
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
lastDomDropAtRef.current = Date.now()
|
lastDomDropAtRef.current = Date.now()
|
||||||
setDragActiveIfChanged(false)
|
setDragActiveIfChanged(false)
|
||||||
if (disabled || isPrompting) return
|
if (disabled) return
|
||||||
const files = Array.from(event.dataTransfer.files ?? [])
|
const files = Array.from(event.dataTransfer.files ?? [])
|
||||||
if (files.length > 0) {
|
if (files.length > 0) {
|
||||||
void appendFilesFromInput(files).catch((error) => {
|
void appendFilesFromInput(files).catch((error) => {
|
||||||
@@ -1002,7 +1074,7 @@ export function MessageInput({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[appendFilesFromInput, disabled, isPrompting, setDragActiveIfChanged]
|
[appendFilesFromInput, disabled, setDragActiveIfChanged]
|
||||||
)
|
)
|
||||||
|
|
||||||
const hasImageAttachments = imageAttachments.length > 0
|
const hasImageAttachments = imageAttachments.length > 0
|
||||||
@@ -1016,7 +1088,7 @@ export function MessageInput({
|
|||||||
? "pt-10"
|
? "pt-10"
|
||||||
: "pt-3"
|
: "pt-3"
|
||||||
const bottomPaddingClass = "pb-10"
|
const bottomPaddingClass = "pb-10"
|
||||||
const showDragActive = isDragActive && !disabled && !isPrompting
|
const showDragActive = isDragActive && !disabled
|
||||||
|
|
||||||
const selectorItems = (
|
const selectorItems = (
|
||||||
<>
|
<>
|
||||||
@@ -1143,7 +1215,7 @@ export function MessageInput({
|
|||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Button
|
<Button
|
||||||
onClick={handlePickFiles}
|
onClick={handlePickFiles}
|
||||||
disabled={disabled || isPrompting}
|
disabled={disabled}
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-6 w-6 shrink-0"
|
className="h-6 w-6 shrink-0"
|
||||||
@@ -1175,16 +1247,47 @@ export function MessageInput({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{isPrompting && onCancel ? (
|
{isEditingQueueItem ? (
|
||||||
|
<div className="absolute right-2 bottom-2 flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
onClick={onCancelQueueEdit}
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
title={tQueue("cancelEdit")}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={!hasSendableContent}
|
||||||
|
size="icon"
|
||||||
|
title={tQueue("saveEdit")}
|
||||||
|
>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : isPrompting && onCancel ? (
|
||||||
|
<div className="absolute right-2 bottom-2 flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
onClick={handleSend}
|
||||||
|
disabled={!hasSendableContent}
|
||||||
|
variant="secondary"
|
||||||
|
size="icon"
|
||||||
|
className="h-8 w-8"
|
||||||
|
title={tQueue("addToQueue")}
|
||||||
|
>
|
||||||
|
<ListPlus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={onCancel}
|
onClick={onCancel}
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="absolute right-2 bottom-2"
|
|
||||||
title={t("cancel")}
|
title={t("cancel")}
|
||||||
>
|
>
|
||||||
<Square className="h-4 w-4" />
|
<Square className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
|
|||||||
121
src/components/chat/message-queue-display.tsx
Normal file
121
src/components/chat/message-queue-display.tsx
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { useCallback, type PointerEvent } from "react"
|
||||||
|
import { Reorder, useDragControls } from "motion/react"
|
||||||
|
import { GripVertical, Pencil, X } from "lucide-react"
|
||||||
|
import { useTranslations } from "next-intl"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import type { QueuedMessage } from "@/hooks/use-message-queue"
|
||||||
|
|
||||||
|
interface MessageQueueDisplayProps {
|
||||||
|
queue: QueuedMessage[]
|
||||||
|
onReorder: (items: QueuedMessage[]) => void
|
||||||
|
onEdit: (id: string) => void
|
||||||
|
onDelete: (id: string) => void
|
||||||
|
editingItemId: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
interface QueueItemProps {
|
||||||
|
item: QueuedMessage
|
||||||
|
index: number
|
||||||
|
isEditing: boolean
|
||||||
|
onEdit: (id: string) => void
|
||||||
|
onDelete: (id: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
function QueueItem({
|
||||||
|
item,
|
||||||
|
index,
|
||||||
|
isEditing,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
}: QueueItemProps) {
|
||||||
|
const t = useTranslations("Folder.chat.messageQueue")
|
||||||
|
const dragControls = useDragControls()
|
||||||
|
|
||||||
|
const startDrag = useCallback(
|
||||||
|
(event: PointerEvent<HTMLButtonElement>) => {
|
||||||
|
event.preventDefault()
|
||||||
|
event.stopPropagation()
|
||||||
|
dragControls.start(event)
|
||||||
|
},
|
||||||
|
[dragControls]
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Reorder.Item
|
||||||
|
as="div"
|
||||||
|
value={item}
|
||||||
|
dragListener={false}
|
||||||
|
dragControls={dragControls}
|
||||||
|
className={cn(
|
||||||
|
"flex items-center gap-1 rounded-md border px-1.5 py-1 text-[10px] leading-none select-none [text-box-trim:both] [text-box-edge:cap_alphabetic]",
|
||||||
|
"bg-muted/40 border-border/70",
|
||||||
|
isEditing && "border-primary/50 bg-primary/5"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="shrink-0 cursor-grab touch-none active:cursor-grabbing p-0"
|
||||||
|
onPointerDown={startDrag}
|
||||||
|
>
|
||||||
|
<GripVertical className="h-3 w-3 text-muted-foreground/60" />
|
||||||
|
</button>
|
||||||
|
<span className="shrink-0 font-mono text-[10px] text-muted-foreground/70">
|
||||||
|
#{index + 1}
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 flex-1 truncate text-[10px] text-foreground/80">
|
||||||
|
{item.draft.displayText}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onEdit(item.id)}
|
||||||
|
className="shrink-0 rounded-sm p-0.5 hover:bg-muted-foreground/15 text-muted-foreground"
|
||||||
|
title={t("editItem")}
|
||||||
|
>
|
||||||
|
<Pencil className="h-2.5 w-2.5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onDelete(item.id)}
|
||||||
|
className="shrink-0 rounded-sm p-0.5 hover:bg-muted-foreground/15 text-muted-foreground"
|
||||||
|
title={t("deleteItem")}
|
||||||
|
>
|
||||||
|
<X className="h-2.5 w-2.5" />
|
||||||
|
</button>
|
||||||
|
</Reorder.Item>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MessageQueueDisplay({
|
||||||
|
queue,
|
||||||
|
onReorder,
|
||||||
|
onEdit,
|
||||||
|
onDelete,
|
||||||
|
editingItemId,
|
||||||
|
}: MessageQueueDisplayProps) {
|
||||||
|
if (queue.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-h-28 overflow-y-auto pb-1">
|
||||||
|
<Reorder.Group
|
||||||
|
as="div"
|
||||||
|
axis="y"
|
||||||
|
values={queue}
|
||||||
|
onReorder={onReorder}
|
||||||
|
className="flex flex-col gap-0.5"
|
||||||
|
>
|
||||||
|
{queue.map((item, index) => (
|
||||||
|
<QueueItem
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
index={index}
|
||||||
|
isEditing={editingItemId === item.id}
|
||||||
|
onEdit={onEdit}
|
||||||
|
onDelete={onDelete}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Reorder.Group>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import { useTabContext } from "@/contexts/tab-context"
|
|||||||
import { useSessionStats } from "@/contexts/session-stats-context"
|
import { useSessionStats } from "@/contexts/session-stats-context"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useConnectionLifecycle } from "@/hooks/use-connection-lifecycle"
|
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 { MessageListView } from "@/components/message/message-list-view"
|
||||||
import { ConversationShell } from "@/components/chat/conversation-shell"
|
import { ConversationShell } from "@/components/chat/conversation-shell"
|
||||||
import { AgentSelector } from "@/components/chat/agent-selector"
|
import { AgentSelector } from "@/components/chat/agent-selector"
|
||||||
@@ -254,6 +255,18 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
disconnect: connDisconnect,
|
disconnect: connDisconnect,
|
||||||
sessionId: connSessionId,
|
sessionId: connSessionId,
|
||||||
} = conn
|
} = 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)
|
const connStatusRef = useRef(connStatus)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
connStatusRef.current = connStatus
|
connStatusRef.current = connStatus
|
||||||
@@ -329,6 +342,32 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
syncTurnMetadata,
|
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(() => {
|
useEffect(() => {
|
||||||
// Only sync non-null liveMessage updates to state. When conn.liveMessage
|
// Only sync non-null liveMessage updates to state. When conn.liveMessage
|
||||||
// goes null (agent finished streaming), don't clear state.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(() => {
|
const handleOpenAgentsSettings = useCallback(() => {
|
||||||
openSettingsWindow("agents", { agentType: selectedAgent }).catch((err) => {
|
openSettingsWindow("agents", { agentType: selectedAgent }).catch((err) => {
|
||||||
console.error(
|
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 showDraftHeader = !hasPersistedConversation && !hasSentMessage
|
||||||
const isWelcomeMode = showDraftHeader
|
const isWelcomeMode = showDraftHeader
|
||||||
|
|
||||||
@@ -657,6 +728,16 @@ const ConversationTabView = memo(function ConversationTabView({
|
|||||||
draftStorageKey={draftStorageKey}
|
draftStorageKey={draftStorageKey}
|
||||||
hideInput={isWelcomeMode}
|
hideInput={isWelcomeMode}
|
||||||
isActive={isActive}
|
isActive={isActive}
|
||||||
|
queue={msgQueue}
|
||||||
|
onEnqueue={mqEnqueue}
|
||||||
|
onQueueReorder={mqReorder}
|
||||||
|
onQueueEdit={handleQueueEdit}
|
||||||
|
onQueueDelete={mqRemove}
|
||||||
|
editingItemId={mqEditingItemId}
|
||||||
|
editingDraftText={editingQueueDraftText}
|
||||||
|
isEditingQueueItem={mqEditingItemId != null}
|
||||||
|
onSaveQueueEdit={handleSaveQueueEdit}
|
||||||
|
onCancelQueueEdit={handleQueueCancelEdit}
|
||||||
>
|
>
|
||||||
{isWelcomeMode ? (
|
{isWelcomeMode ? (
|
||||||
<div className="flex h-full min-h-0 flex-col items-center justify-center">
|
<div className="flex h-full min-h-0 flex-col items-center justify-center">
|
||||||
|
|||||||
89
src/hooks/use-message-queue.ts
Normal file
89
src/hooks/use-message-queue.ts
Normal file
@@ -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<QueuedMessage[]>([])
|
||||||
|
const [editingItemId, setEditingItemId] = useState<string | null>(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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1145,6 +1145,13 @@
|
|||||||
"cancel": "إلغاء",
|
"cancel": "إلغاء",
|
||||||
"send": "إرسال"
|
"send": "إرسال"
|
||||||
},
|
},
|
||||||
|
"messageQueue": {
|
||||||
|
"addToQueue": "إضافة للقائمة",
|
||||||
|
"saveEdit": "حفظ",
|
||||||
|
"cancelEdit": "إلغاء التعديل",
|
||||||
|
"editItem": "تعديل",
|
||||||
|
"deleteItem": "حذف"
|
||||||
|
},
|
||||||
"welcomeInputPanel": {
|
"welcomeInputPanel": {
|
||||||
"agentsSettingsPath": "الإعدادات > الوكلاء",
|
"agentsSettingsPath": "الإعدادات > الوكلاء",
|
||||||
"autoConnectFallback": "انقر لفتح {path} وإدارة التثبيت.",
|
"autoConnectFallback": "انقر لفتح {path} وإدارة التثبيت.",
|
||||||
|
|||||||
@@ -1145,6 +1145,13 @@
|
|||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"send": "Senden"
|
"send": "Senden"
|
||||||
},
|
},
|
||||||
|
"messageQueue": {
|
||||||
|
"addToQueue": "Zur Warteschlange",
|
||||||
|
"saveEdit": "Speichern",
|
||||||
|
"cancelEdit": "Bearbeitung abbrechen",
|
||||||
|
"editItem": "Bearbeiten",
|
||||||
|
"deleteItem": "Entfernen"
|
||||||
|
},
|
||||||
"welcomeInputPanel": {
|
"welcomeInputPanel": {
|
||||||
"agentsSettingsPath": "Einstellungen > Agenten",
|
"agentsSettingsPath": "Einstellungen > Agenten",
|
||||||
"autoConnectFallback": "Klicken Sie, um {path} zu öffnen und die Installation zu verwalten.",
|
"autoConnectFallback": "Klicken Sie, um {path} zu öffnen und die Installation zu verwalten.",
|
||||||
|
|||||||
@@ -1145,6 +1145,13 @@
|
|||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"send": "Send"
|
"send": "Send"
|
||||||
},
|
},
|
||||||
|
"messageQueue": {
|
||||||
|
"addToQueue": "Queue message",
|
||||||
|
"saveEdit": "Save",
|
||||||
|
"cancelEdit": "Cancel edit",
|
||||||
|
"editItem": "Edit",
|
||||||
|
"deleteItem": "Remove"
|
||||||
|
},
|
||||||
"welcomeInputPanel": {
|
"welcomeInputPanel": {
|
||||||
"agentsSettingsPath": "Settings > Agents",
|
"agentsSettingsPath": "Settings > Agents",
|
||||||
"autoConnectFallback": "Click to open {path} and manage installation.",
|
"autoConnectFallback": "Click to open {path} and manage installation.",
|
||||||
|
|||||||
@@ -1145,6 +1145,13 @@
|
|||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"send": "Enviar"
|
"send": "Enviar"
|
||||||
},
|
},
|
||||||
|
"messageQueue": {
|
||||||
|
"addToQueue": "Agregar a la cola",
|
||||||
|
"saveEdit": "Guardar",
|
||||||
|
"cancelEdit": "Cancelar edición",
|
||||||
|
"editItem": "Editar",
|
||||||
|
"deleteItem": "Eliminar"
|
||||||
|
},
|
||||||
"welcomeInputPanel": {
|
"welcomeInputPanel": {
|
||||||
"agentsSettingsPath": "Ajustes > Agentes",
|
"agentsSettingsPath": "Ajustes > Agentes",
|
||||||
"autoConnectFallback": "Haz clic para abrir {path} y gestionar la instalación.",
|
"autoConnectFallback": "Haz clic para abrir {path} y gestionar la instalación.",
|
||||||
|
|||||||
@@ -1145,6 +1145,13 @@
|
|||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"send": "Envoyer"
|
"send": "Envoyer"
|
||||||
},
|
},
|
||||||
|
"messageQueue": {
|
||||||
|
"addToQueue": "Mettre en file",
|
||||||
|
"saveEdit": "Enregistrer",
|
||||||
|
"cancelEdit": "Annuler la modification",
|
||||||
|
"editItem": "Modifier",
|
||||||
|
"deleteItem": "Supprimer"
|
||||||
|
},
|
||||||
"welcomeInputPanel": {
|
"welcomeInputPanel": {
|
||||||
"agentsSettingsPath": "Paramètres > Agents",
|
"agentsSettingsPath": "Paramètres > Agents",
|
||||||
"autoConnectFallback": "Cliquez pour ouvrir {path} et gérer l'installation.",
|
"autoConnectFallback": "Cliquez pour ouvrir {path} et gérer l'installation.",
|
||||||
|
|||||||
@@ -1145,6 +1145,13 @@
|
|||||||
"cancel": "キャンセル",
|
"cancel": "キャンセル",
|
||||||
"send": "送信"
|
"send": "送信"
|
||||||
},
|
},
|
||||||
|
"messageQueue": {
|
||||||
|
"addToQueue": "キューに追加",
|
||||||
|
"saveEdit": "保存",
|
||||||
|
"cancelEdit": "編集をキャンセル",
|
||||||
|
"editItem": "編集",
|
||||||
|
"deleteItem": "削除"
|
||||||
|
},
|
||||||
"welcomeInputPanel": {
|
"welcomeInputPanel": {
|
||||||
"agentsSettingsPath": "設定 > エージェント",
|
"agentsSettingsPath": "設定 > エージェント",
|
||||||
"autoConnectFallback": "{path} を開いてインストールを管理してください。",
|
"autoConnectFallback": "{path} を開いてインストールを管理してください。",
|
||||||
|
|||||||
@@ -1145,6 +1145,13 @@
|
|||||||
"cancel": "취소",
|
"cancel": "취소",
|
||||||
"send": "보내기"
|
"send": "보내기"
|
||||||
},
|
},
|
||||||
|
"messageQueue": {
|
||||||
|
"addToQueue": "대기열에 추가",
|
||||||
|
"saveEdit": "저장",
|
||||||
|
"cancelEdit": "편집 취소",
|
||||||
|
"editItem": "편집",
|
||||||
|
"deleteItem": "삭제"
|
||||||
|
},
|
||||||
"welcomeInputPanel": {
|
"welcomeInputPanel": {
|
||||||
"agentsSettingsPath": "설정 > 에이전트",
|
"agentsSettingsPath": "설정 > 에이전트",
|
||||||
"autoConnectFallback": "{path}을(를) 열어 설치를 관리하세요.",
|
"autoConnectFallback": "{path}을(를) 열어 설치를 관리하세요.",
|
||||||
|
|||||||
@@ -1145,6 +1145,13 @@
|
|||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"send": "Enviar"
|
"send": "Enviar"
|
||||||
},
|
},
|
||||||
|
"messageQueue": {
|
||||||
|
"addToQueue": "Adicionar à fila",
|
||||||
|
"saveEdit": "Salvar",
|
||||||
|
"cancelEdit": "Cancelar edição",
|
||||||
|
"editItem": "Editar",
|
||||||
|
"deleteItem": "Remover"
|
||||||
|
},
|
||||||
"welcomeInputPanel": {
|
"welcomeInputPanel": {
|
||||||
"agentsSettingsPath": "Configurações > Agentes",
|
"agentsSettingsPath": "Configurações > Agentes",
|
||||||
"autoConnectFallback": "Clique para abrir {path} e gerenciar a instalação.",
|
"autoConnectFallback": "Clique para abrir {path} e gerenciar a instalação.",
|
||||||
|
|||||||
@@ -1145,6 +1145,13 @@
|
|||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"send": "发送"
|
"send": "发送"
|
||||||
},
|
},
|
||||||
|
"messageQueue": {
|
||||||
|
"addToQueue": "加入队列",
|
||||||
|
"saveEdit": "保存",
|
||||||
|
"cancelEdit": "取消编辑",
|
||||||
|
"editItem": "编辑",
|
||||||
|
"deleteItem": "删除"
|
||||||
|
},
|
||||||
"welcomeInputPanel": {
|
"welcomeInputPanel": {
|
||||||
"agentsSettingsPath": "设置 > Agents",
|
"agentsSettingsPath": "设置 > Agents",
|
||||||
"autoConnectFallback": "点击前往 {path} 管理安装。",
|
"autoConnectFallback": "点击前往 {path} 管理安装。",
|
||||||
|
|||||||
@@ -1145,6 +1145,13 @@
|
|||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"send": "傳送"
|
"send": "傳送"
|
||||||
},
|
},
|
||||||
|
"messageQueue": {
|
||||||
|
"addToQueue": "加入佇列",
|
||||||
|
"saveEdit": "儲存",
|
||||||
|
"cancelEdit": "取消編輯",
|
||||||
|
"editItem": "編輯",
|
||||||
|
"deleteItem": "刪除"
|
||||||
|
},
|
||||||
"welcomeInputPanel": {
|
"welcomeInputPanel": {
|
||||||
"agentsSettingsPath": "設定 > Agents",
|
"agentsSettingsPath": "設定 > Agents",
|
||||||
"autoConnectFallback": "點擊前往 {path} 管理安裝。",
|
"autoConnectFallback": "點擊前往 {path} 管理安裝。",
|
||||||
|
|||||||
Reference in New Issue
Block a user