支持会话响应时添加队列消息

This commit is contained in:
xintaofei
2026-03-12 23:19:24 +08:00
parent 6c26d067fd
commit 1f623c0d6e
16 changed files with 566 additions and 31 deletions

View File

@@ -9,7 +9,9 @@ import type {
SessionModeInfo,
AvailableCommandInfo,
} from "@/lib/types"
import type { QueuedMessage } from "@/hooks/use-message-queue"
import { MessageInput } from "@/components/chat/message-input"
import { MessageQueueDisplay } from "@/components/chat/message-queue-display"
interface ChatInputProps {
status: ConnectionStatus | null
@@ -29,6 +31,16 @@ interface ChatInputProps {
attachmentTabId?: string | null
draftStorageKey?: string | null
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({
@@ -49,6 +61,16 @@ export function ChatInput({
attachmentTabId,
draftStorageKey,
isActive,
queue,
onEnqueue,
onQueueReorder,
onQueueEdit,
onQueueDelete,
editingItemId,
editingDraftText,
isEditingQueueItem,
onSaveQueueEdit,
onCancelQueueEdit,
}: ChatInputProps) {
const t = useTranslations("Folder.chat.chatInput")
const isConnected = status === "connected"
@@ -57,12 +79,25 @@ export function ChatInput({
return (
<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
onSend={onSend}
promptCapabilities={promptCapabilities}
onFocus={onFocus}
defaultPath={defaultPath}
disabled={!isConnected}
disabled={!isConnected && !isPrompting}
isPrompting={isPrompting}
onCancel={onCancel}
modes={modes}
@@ -76,6 +111,11 @@ export function ChatInput({
attachmentTabId={attachmentTabId}
draftStorageKey={draftStorageKey}
isActive={isActive}
onEnqueue={onEnqueue}
editingDraftText={editingDraftText}
isEditingQueueItem={isEditingQueueItem}
onSaveQueueEdit={onSaveQueueEdit}
onCancelQueueEdit={onCancelQueueEdit}
placeholder={
isConnecting
? t("connecting")

View File

@@ -11,6 +11,7 @@ import type {
PendingPermission,
PendingQuestion,
} from "@/contexts/acp-connections-context"
import type { QueuedMessage } from "@/hooks/use-message-queue"
import { ChatInput } from "@/components/chat/chat-input"
import { PermissionDialog } from "@/components/chat/permission-dialog"
import { QuestionDialog } from "@/components/chat/question-dialog"
@@ -40,6 +41,16 @@ interface ConversationShellProps {
draftStorageKey?: string | null
hideInput?: 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({
@@ -67,6 +78,16 @@ export function ConversationShell({
draftStorageKey,
hideInput = false,
isActive,
queue,
onEnqueue,
onQueueReorder,
onQueueEdit,
onQueueDelete,
editingItemId,
editingDraftText,
isEditingQueueItem,
onSaveQueueEdit,
onCancelQueueEdit,
}: ConversationShellProps) {
return (
<div className="flex h-full min-h-0 flex-col">
@@ -98,6 +119,16 @@ export function ConversationShell({
attachmentTabId={attachmentTabId}
draftStorageKey={draftStorageKey}
isActive={isActive}
queue={queue}
onEnqueue={onEnqueue}
onQueueReorder={onQueueReorder}
onQueueEdit={onQueueEdit}
onQueueDelete={onQueueDelete}
editingItemId={editingItemId}
editingDraftText={editingDraftText}
isEditingQueueItem={isEditingQueueItem}
onSaveQueueEdit={onSaveQueueEdit}
onCancelQueueEdit={onCancelQueueEdit}
/>
)}

View File

@@ -13,7 +13,16 @@ import {
PopoverTrigger,
} from "@/components/ui/popover"
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 { matchShortcutEvent } from "@/lib/keyboard-shortcuts"
import { useShortcutSettings } from "@/hooks/use-shortcut-settings"
@@ -62,6 +71,11 @@ interface MessageInputProps {
attachmentTabId?: string | null
draftStorageKey?: string | null
isActive?: boolean
onEnqueue?: (draft: PromptDraft, modeId: string | null) => void
editingDraftText?: string | null
isEditingQueueItem?: boolean
onSaveQueueEdit?: (draft: PromptDraft) => void
onCancelQueueEdit?: () => void
}
interface ResourceInputAttachment {
@@ -261,8 +275,14 @@ export function MessageInput({
attachmentTabId,
draftStorageKey,
isActive = false,
onEnqueue,
editingDraftText,
isEditingQueueItem = false,
onSaveQueueEdit,
onCancelQueueEdit,
}: MessageInputProps) {
const t = useTranslations("Folder.chat.messageInput")
const tQueue = useTranslations("Folder.chat.messageQueue")
const { shortcuts } = useShortcutSettings()
const effectiveDraftStorageKey = draftStorageKey ?? attachmentTabId ?? null
const resolvedPlaceholder = placeholder ?? t("askAnything")
@@ -302,6 +322,24 @@ export function MessageInput({
isPromptingRef.current = 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) => {
if (dragActiveRef.current === next) return
dragActiveRef.current = next
@@ -309,9 +347,9 @@ export function MessageInput({
}, [])
useEffect(() => {
if (!effectiveDraftStorageKey) return
if (!effectiveDraftStorageKey || isEditingQueueItem) return
saveMessageInputDraft(effectiveDraftStorageKey, text)
}, [effectiveDraftStorageKey, text])
}, [effectiveDraftStorageKey, text, isEditingQueueItem])
const availableModes = useMemo(() => modes ?? [], [modes])
const availableConfigOptions = useMemo(
@@ -653,7 +691,7 @@ export function MessageInput({
const handlePaste = useCallback(
(event: React.ClipboardEvent<HTMLTextAreaElement>) => {
if (disabled || isPrompting) return
if (disabled) return
const files = Array.from(event.clipboardData?.files ?? [])
if (files.length === 0) return
event.preventDefault()
@@ -661,7 +699,7 @@ export function MessageInput({
console.error("[MessageInput] paste files failed:", error)
})
},
[appendFilesFromInput, disabled, isPrompting]
[appendFilesFromInput, disabled]
)
useEffect(() => {
@@ -763,15 +801,13 @@ export function MessageInput({
if (payload.type === "drop") {
setDragActiveIfChanged(false)
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) => {
console.error("[MessageInput] drag drop paths failed:", error)
})
return
}
setDragActiveIfChanged(
inside && !disabledRef.current && !isPromptingRef.current
)
setDragActiveIfChanged(inside && !disabledRef.current)
}
const setup = async () => {
@@ -843,9 +879,9 @@ export function MessageInput({
setAttachments((prev) => prev.filter((item) => item.id !== id))
}, [])
const handleSend = useCallback(() => {
const buildDraft = useCallback((): PromptDraft | null => {
const trimmed = textRef.current.trim()
if (!trimmed && attachments.length === 0) return
if (!trimmed && attachments.length === 0) return null
const blocks: PromptInputBlock[] = []
if (trimmed) {
@@ -883,14 +919,41 @@ export function MessageInput({
const displayText =
trimmed ||
`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) {
clearMessageInputDraft(effectiveDraftStorageKey)
}
setText("")
setAttachments([])
}, [
attachments,
buildDraft,
isEditingQueueItem,
isPrompting,
onSaveQueueEdit,
onEnqueue,
onSend,
effectiveModeId,
showModeSelector,
@@ -935,9 +998,15 @@ export function MessageInput({
}
}
if (isEditingQueueItem && e.key === "Escape") {
e.preventDefault()
onCancelQueueEdit?.()
return
}
if (matchShortcutEvent(e, shortcuts.send_message)) {
e.preventDefault()
if (!disabled) handleSend()
if (!disabled || isPrompting || isEditingQueueItem) handleSend()
} else if (matchShortcutEvent(e, shortcuts.newline_in_message)) {
e.preventDefault()
const textarea = e.currentTarget as HTMLTextAreaElement
@@ -953,6 +1022,9 @@ export function MessageInput({
},
[
disabled,
isPrompting,
isEditingQueueItem,
onCancelQueueEdit,
handleSend,
shortcuts,
slashMenuOpen,
@@ -966,11 +1038,11 @@ export function MessageInput({
(event: React.DragEvent<HTMLDivElement>) => {
if (!hasDragFiles(event.dataTransfer)) return
event.preventDefault()
if (!disabled && !isPrompting) {
if (!disabled) {
setDragActiveIfChanged(true)
}
},
[disabled, isPrompting, setDragActiveIfChanged]
[disabled, setDragActiveIfChanged]
)
const handleContainerDragLeave = useCallback(
@@ -994,7 +1066,7 @@ export function MessageInput({
event.preventDefault()
lastDomDropAtRef.current = Date.now()
setDragActiveIfChanged(false)
if (disabled || isPrompting) return
if (disabled) return
const files = Array.from(event.dataTransfer.files ?? [])
if (files.length > 0) {
void appendFilesFromInput(files).catch((error) => {
@@ -1002,7 +1074,7 @@ export function MessageInput({
})
}
},
[appendFilesFromInput, disabled, isPrompting, setDragActiveIfChanged]
[appendFilesFromInput, disabled, setDragActiveIfChanged]
)
const hasImageAttachments = imageAttachments.length > 0
@@ -1016,7 +1088,7 @@ export function MessageInput({
? "pt-10"
: "pt-3"
const bottomPaddingClass = "pb-10"
const showDragActive = isDragActive && !disabled && !isPrompting
const showDragActive = isDragActive && !disabled
const selectorItems = (
<>
@@ -1143,7 +1215,7 @@ export function MessageInput({
<div className="flex items-center gap-1">
<Button
onClick={handlePickFiles}
disabled={disabled || isPrompting}
disabled={disabled}
variant="ghost"
size="icon"
className="h-6 w-6 shrink-0"
@@ -1175,16 +1247,47 @@ export function MessageInput({
)}
</div>
</div>
{isPrompting && onCancel ? (
<Button
onClick={onCancel}
variant="destructive"
size="icon"
className="absolute right-2 bottom-2"
title={t("cancel")}
>
<Square className="h-4 w-4" />
</Button>
{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
onClick={onCancel}
variant="destructive"
size="icon"
title={t("cancel")}
>
<Square className="h-4 w-4" />
</Button>
</div>
) : (
<Button
onClick={handleSend}

View 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>
)
}

View File

@@ -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 ? (
<div className="flex h-full min-h-0 flex-col items-center justify-center">

View 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,
}
}

View File

@@ -1145,6 +1145,13 @@
"cancel": "إلغاء",
"send": "إرسال"
},
"messageQueue": {
"addToQueue": "إضافة للقائمة",
"saveEdit": "حفظ",
"cancelEdit": "إلغاء التعديل",
"editItem": "تعديل",
"deleteItem": "حذف"
},
"welcomeInputPanel": {
"agentsSettingsPath": "الإعدادات > الوكلاء",
"autoConnectFallback": "انقر لفتح {path} وإدارة التثبيت.",

View File

@@ -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.",

View File

@@ -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.",

View File

@@ -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.",

View File

@@ -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.",

View File

@@ -1145,6 +1145,13 @@
"cancel": "キャンセル",
"send": "送信"
},
"messageQueue": {
"addToQueue": "キューに追加",
"saveEdit": "保存",
"cancelEdit": "編集をキャンセル",
"editItem": "編集",
"deleteItem": "削除"
},
"welcomeInputPanel": {
"agentsSettingsPath": "設定 > エージェント",
"autoConnectFallback": "{path} を開いてインストールを管理してください。",

View File

@@ -1145,6 +1145,13 @@
"cancel": "취소",
"send": "보내기"
},
"messageQueue": {
"addToQueue": "대기열에 추가",
"saveEdit": "저장",
"cancelEdit": "편집 취소",
"editItem": "편집",
"deleteItem": "삭제"
},
"welcomeInputPanel": {
"agentsSettingsPath": "설정 > 에이전트",
"autoConnectFallback": "{path}을(를) 열어 설치를 관리하세요.",

View File

@@ -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.",

View File

@@ -1145,6 +1145,13 @@
"cancel": "取消",
"send": "发送"
},
"messageQueue": {
"addToQueue": "加入队列",
"saveEdit": "保存",
"cancelEdit": "取消编辑",
"editItem": "编辑",
"deleteItem": "删除"
},
"welcomeInputPanel": {
"agentsSettingsPath": "设置 > Agents",
"autoConnectFallback": "点击前往 {path} 管理安装。",

View File

@@ -1145,6 +1145,13 @@
"cancel": "取消",
"send": "傳送"
},
"messageQueue": {
"addToQueue": "加入佇列",
"saveEdit": "儲存",
"cancelEdit": "取消編輯",
"editItem": "編輯",
"deleteItem": "刪除"
},
"welcomeInputPanel": {
"agentsSettingsPath": "設定 > Agents",
"autoConnectFallback": "點擊前往 {path} 管理安裝。",