Просмотр исходного кода

feat: Add support for message queueing (#6167)

* feat: Add support for message queueing

* fix: address PR review feedback for message queueing feature

- Restore original ChatView tests from main branch
- Fix broken test by updating ChatTextArea mock
- Add comprehensive tests for message queueing (simplified due to mocking constraints)
- Fix race condition using useRef and setTimeout in queue processing
- Extract QueuedMessage interface to shared types.ts file
- Replace inline styles with Tailwind classes in QueuedMessages
- Add i18n support for 'Queued Messages:' text
- Add keyboard navigation for removing queued messages
- Add JSDoc for fromQueue parameter in handleSendMessage

* refactor: move QueuedMessage interface to packages/types

- Move QueuedMessage interface from local types.ts to packages/types/src/message.ts
- Update imports in ChatView.tsx and QueuedMessages.tsx to use @roo-code/types
- Remove local types.ts file to follow codebase conventions

* fix: add id field when creating queued messages

- Generate unique id using timestamp when adding messages to queue
- Fixes TypeScript error after moving QueuedMessage interface

* Stop disabling sending

* Translations

* Fix tests

* Improved styling

* Remove unused string

* Test cleanup

* fix: address message queueing issues

- Fix race condition in queue processing by re-checking queue state inside setTimeout
- Add error handling for queue operations with retry mechanism
- Replace array index with stable message.id for React keys in QueuedMessages
- Generate more unique IDs using timestamp + random component

* feat: add inline editing for queued messages

- Add ability to edit queued messages by clicking on them
- Support Enter to save and Escape to cancel edits
- Add textarea that auto-resizes based on content
- Add hover effect to indicate messages are editable
- Add translation for click to edit tooltip

* feat: add scrollbar and fix height for queued messages

- Add max-height of 300px with scrollbar to queue container
- Add flex-shrink-0 to prevent message items from being squished
- Ensure consistent height for message items when multiple messages are queued

* feat: add translations for queued message edit tooltip

- Add 'queuedMessages.clickToEdit' translation key to all 18 language files
- Provides localized tooltip text for the click-to-edit functionality

* fix: improve message queue processing reliability

- Fix race condition by removing nested setState and setTimeout
- Add retry limit (3 attempts) to prevent infinite loops
- Add proper cleanup on component unmount
- Clear queue when starting new task
- Prevent queue processing during API errors
- Fix ESLint warnings for React hooks dependencies

---------

Co-authored-by: Roo Code <[email protected]>
Co-authored-by: Daniel Riccio <[email protected]>
Co-authored-by: Matt Rubens <[email protected]>
roomote[bot] 5 месяцев назад
Родитель
Сommit
f546e6ed1d

+ 16 - 0
packages/types/src/message.ts

@@ -174,3 +174,19 @@ export const tokenUsageSchema = z.object({
 })
 
 export type TokenUsage = z.infer<typeof tokenUsageSchema>
+
+/**
+ * QueuedMessage
+ */
+
+/**
+ * Represents a message that is queued to be sent when sending is enabled
+ */
+export interface QueuedMessage {
+	/** Unique identifier for the queued message */
+	id: string
+	/** The text content of the message */
+	text: string
+	/** Array of image data URLs attached to the message */
+	images: string[]
+}

+ 10 - 21
webview-ui/src/components/chat/ChatTextArea.tsx

@@ -204,10 +204,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 		}, [selectedType, searchQuery])
 
 		const handleEnhancePrompt = useCallback(() => {
-			if (sendingDisabled) {
-				return
-			}
-
 			const trimmedInput = inputValue.trim()
 
 			if (trimmedInput) {
@@ -216,7 +212,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 			} else {
 				setInputValue(t("chat:enhancePromptDescription"))
 			}
-		}, [inputValue, sendingDisabled, setInputValue, t])
+		}, [inputValue, setInputValue, t])
 
 		const allModes = useMemo(() => getAllModes(customModes), [customModes])
 
@@ -436,11 +432,9 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 				if (event.key === "Enter" && !event.shiftKey && !isComposing) {
 					event.preventDefault()
 
-					if (!sendingDisabled) {
-						// Reset history navigation state when sending
-						resetHistoryNavigation()
-						onSend()
-					}
+					// Always call onSend - let ChatView handle queueing when disabled
+					resetHistoryNavigation()
+					onSend()
 				}
 
 				if (event.key === "Backspace" && !isComposing) {
@@ -488,7 +482,6 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 				}
 			},
 			[
-				sendingDisabled,
 				onSend,
 				showContextMenu,
 				searchQuery,
@@ -1034,8 +1027,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 					<StandardTooltip content={t("chat:enhancePrompt")}>
 						<button
 							aria-label={t("chat:enhancePrompt")}
-							disabled={sendingDisabled}
-							onClick={!sendingDisabled ? handleEnhancePrompt : undefined}
+							disabled={false}
+							onClick={handleEnhancePrompt}
 							className={cn(
 								"relative inline-flex items-center justify-center",
 								"bg-transparent border-none p-1.5",
@@ -1045,9 +1038,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 								"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
 								"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
 								"active:bg-[rgba(255,255,255,0.1)]",
-								!sendingDisabled && "cursor-pointer",
-								sendingDisabled &&
-									"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
+								"cursor-pointer",
 							)}>
 							<WandSparkles className={cn("w-4 h-4", isEnhancingPrompt && "animate-spin")} />
 						</button>
@@ -1059,8 +1050,8 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 						<StandardTooltip content={t("chat:sendMessage")}>
 							<button
 								aria-label={t("chat:sendMessage")}
-								disabled={sendingDisabled}
-								onClick={!sendingDisabled ? onSend : undefined}
+								disabled={false}
+								onClick={onSend}
 								className={cn(
 									"relative inline-flex items-center justify-center",
 									"bg-transparent border-none p-1.5",
@@ -1070,9 +1061,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
 									"hover:bg-[rgba(255,255,255,0.03)] hover:border-[rgba(255,255,255,0.15)]",
 									"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
 									"active:bg-[rgba(255,255,255,0.1)]",
-									!sendingDisabled && "cursor-pointer",
-									sendingDisabled &&
-										"opacity-40 cursor-not-allowed grayscale-[30%] hover:bg-transparent hover:border-[rgba(255,255,255,0.08)] active:bg-transparent",
+									"cursor-pointer",
 								)}>
 								<SendHorizontal className="w-4 h-4" />
 							</button>

+ 151 - 33
webview-ui/src/components/chat/ChatView.tsx

@@ -54,7 +54,9 @@ import AutoApproveMenu from "./AutoApproveMenu"
 import SystemPromptWarning from "./SystemPromptWarning"
 import ProfileViolationWarning from "./ProfileViolationWarning"
 import { CheckpointWarning } from "./CheckpointWarning"
+import QueuedMessages from "./QueuedMessages"
 import { getLatestTodo } from "@roo/todo"
+import { QueuedMessage } from "@roo-code/types"
 
 export interface ChatViewProps {
 	isHidden: boolean
@@ -154,6 +156,10 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 	const textAreaRef = useRef<HTMLTextAreaElement>(null)
 	const [sendingDisabled, setSendingDisabled] = useState(false)
 	const [selectedImages, setSelectedImages] = useState<string[]>([])
+	const [messageQueue, setMessageQueue] = useState<QueuedMessage[]>([])
+	const isProcessingQueueRef = useRef(false)
+	const retryCountRef = useRef<Map<string, number>>(new Map())
+	const MAX_RETRY_ATTEMPTS = 3
 
 	// we need to hold on to the ask because useEffect > lastMessage will always let us know when an ask comes in and handle it, but by the time handleMessage is called, the last message might not be the ask anymore (it could be a say that followed)
 	const [clineAsk, setClineAsk] = useState<ClineAsk | undefined>(undefined)
@@ -439,6 +445,11 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		}
 		// Reset user response flag for new task
 		userRespondedRef.current = false
+
+		// Clear message queue when starting a new task
+		setMessageQueue([])
+		// Clear retry counts
+		retryCountRef.current.clear()
 	}, [task?.ts])
 
 	useEffect(() => {
@@ -538,47 +549,133 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		disableAutoScrollRef.current = false
 	}, [])
 
+	/**
+	 * Handles sending messages to the extension
+	 * @param text - The message text to send
+	 * @param images - Array of image data URLs to send with the message
+	 * @param fromQueue - Internal flag indicating if this message is being sent from the queue (prevents re-queueing)
+	 */
 	const handleSendMessage = useCallback(
-		(text: string, images: string[]) => {
-			text = text.trim()
-
-			if (text || images.length > 0) {
-				// Mark that user has responded - this prevents any pending auto-approvals
-				userRespondedRef.current = true
-
-				if (messagesRef.current.length === 0) {
-					vscode.postMessage({ type: "newTask", text, images })
-				} else if (clineAskRef.current) {
-					if (clineAskRef.current === "followup") {
-						markFollowUpAsAnswered()
+		(text: string, images: string[], fromQueue = false) => {
+			try {
+				text = text.trim()
+
+				if (text || images.length > 0) {
+					if (sendingDisabled && !fromQueue) {
+						// Generate a more unique ID using timestamp + random component
+						const messageId = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
+						setMessageQueue((prev) => [...prev, { id: messageId, text, images }])
+						setInputValue("")
+						setSelectedImages([])
+						return
 					}
+					// Mark that user has responded - this prevents any pending auto-approvals
+					userRespondedRef.current = true
+
+					if (messagesRef.current.length === 0) {
+						vscode.postMessage({ type: "newTask", text, images })
+					} else if (clineAskRef.current) {
+						if (clineAskRef.current === "followup") {
+							markFollowUpAsAnswered()
+						}
 
-					// Use clineAskRef.current
-					switch (
-						clineAskRef.current // Use clineAskRef.current
-					) {
-						case "followup":
-						case "tool":
-						case "browser_action_launch":
-						case "command": // User can provide feedback to a tool or command use.
-						case "command_output": // User can send input to command stdin.
-						case "use_mcp_server":
-						case "completion_result": // If this happens then the user has feedback for the completion result.
-						case "resume_task":
-						case "resume_completed_task":
-						case "mistake_limit_reached":
-							vscode.postMessage({ type: "askResponse", askResponse: "messageResponse", text, images })
-							break
-						// There is no other case that a textfield should be enabled.
+						// Use clineAskRef.current
+						switch (
+							clineAskRef.current // Use clineAskRef.current
+						) {
+							case "followup":
+							case "tool":
+							case "browser_action_launch":
+							case "command": // User can provide feedback to a tool or command use.
+							case "command_output": // User can send input to command stdin.
+							case "use_mcp_server":
+							case "completion_result": // If this happens then the user has feedback for the completion result.
+							case "resume_task":
+							case "resume_completed_task":
+							case "mistake_limit_reached":
+								vscode.postMessage({
+									type: "askResponse",
+									askResponse: "messageResponse",
+									text,
+									images,
+								})
+								break
+							// There is no other case that a textfield should be enabled.
+						}
+					} else {
+						// This is a new message in an ongoing task.
+						vscode.postMessage({ type: "askResponse", askResponse: "messageResponse", text, images })
 					}
-				}
 
-				handleChatReset()
+					handleChatReset()
+				}
+			} catch (error) {
+				console.error("Error in handleSendMessage:", error)
+				// If this was a queued message, we should handle it differently
+				if (fromQueue) {
+					throw error // Re-throw to be caught by the queue processor
+				}
+				// For direct sends, we could show an error to the user
+				// but for now we'll just log it
 			}
 		},
-		[handleChatReset, markFollowUpAsAnswered], // messagesRef and clineAskRef are stable
+		[handleChatReset, markFollowUpAsAnswered, sendingDisabled], // messagesRef and clineAskRef are stable
 	)
 
+	useEffect(() => {
+		// Early return if conditions aren't met
+		// Also don't process queue if there's an API error (clineAsk === "api_req_failed")
+		if (
+			sendingDisabled ||
+			messageQueue.length === 0 ||
+			isProcessingQueueRef.current ||
+			clineAsk === "api_req_failed"
+		) {
+			return
+		}
+
+		// Mark as processing immediately to prevent race conditions
+		isProcessingQueueRef.current = true
+
+		// Process the first message in the queue
+		const [nextMessage, ...remaining] = messageQueue
+
+		// Update queue immediately to prevent duplicate processing
+		setMessageQueue(remaining)
+
+		// Process the message
+		Promise.resolve()
+			.then(() => {
+				handleSendMessage(nextMessage.text, nextMessage.images, true)
+				// Clear retry count on success
+				retryCountRef.current.delete(nextMessage.id)
+			})
+			.catch((error) => {
+				console.error("Failed to send queued message:", error)
+
+				// Get current retry count
+				const retryCount = retryCountRef.current.get(nextMessage.id) || 0
+
+				// Only re-add if under retry limit
+				if (retryCount < MAX_RETRY_ATTEMPTS) {
+					retryCountRef.current.set(nextMessage.id, retryCount + 1)
+					// Re-add the message to the end of the queue
+					setMessageQueue((current) => [...current, nextMessage])
+				} else {
+					console.error(`Message ${nextMessage.id} failed after ${MAX_RETRY_ATTEMPTS} attempts, discarding`)
+					retryCountRef.current.delete(nextMessage.id)
+				}
+			})
+			.finally(() => {
+				isProcessingQueueRef.current = false
+			})
+
+		// Cleanup function to handle component unmount
+		return () => {
+			isProcessingQueueRef.current = false
+		}
+	}, [sendingDisabled, messageQueue, handleSendMessage, clineAsk])
+
 	const handleSetChatBoxMessage = useCallback(
 		(text: string, images: string[]) => {
 			// Avoid nested template literals by breaking down the logic
@@ -594,6 +691,18 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 		[inputValue, selectedImages],
 	)
 
+	// Cleanup retry count map on unmount
+	useEffect(() => {
+		// Store refs in variables to avoid stale closure issues
+		const retryCountMap = retryCountRef.current
+		const isProcessingRef = isProcessingQueueRef
+
+		return () => {
+			retryCountMap.clear()
+			isProcessingRef.current = false
+		}
+	}, [])
+
 	const startNewTask = useCallback(() => vscode.postMessage({ type: "clearTask" }), [])
 
 	// This logic depends on the useEffect[messages] above to set clineAsk,
@@ -1630,7 +1739,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 	const areButtonsVisible = showScrollToBottom || primaryButtonText || secondaryButtonText || isStreaming
 
 	return (
-		<div className={isHidden ? "hidden" : "fixed top-0 left-0 right-0 bottom-0 flex flex-col overflow-hidden"}>
+		<div
+			data-testid="chat-view"
+			className={isHidden ? "hidden" : "fixed top-0 left-0 right-0 bottom-0 flex flex-col overflow-hidden"}>
 			{(showAnnouncement || showAnnouncementModal) && (
 				<Announcement
 					hideAnnouncement={() => {
@@ -1836,6 +1947,13 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
 				</>
 			)}
 
+			<QueuedMessages
+				queue={messageQueue}
+				onRemove={(index) => setMessageQueue((prev) => prev.filter((_, i) => i !== index))}
+				onUpdate={(index, newText) => {
+					setMessageQueue((prev) => prev.map((msg, i) => (i === index ? { ...msg, text: newText } : msg)))
+				}}
+			/>
 			<ChatTextArea
 				ref={textAreaRef}
 				inputValue={inputValue}

+ 112 - 0
webview-ui/src/components/chat/QueuedMessages.tsx

@@ -0,0 +1,112 @@
+import React, { useState } from "react"
+import { useTranslation } from "react-i18next"
+import Thumbnails from "../common/Thumbnails"
+import { QueuedMessage } from "@roo-code/types"
+import { Mention } from "./Mention"
+import { Button } from "@src/components/ui"
+
+interface QueuedMessagesProps {
+	queue: QueuedMessage[]
+	onRemove: (index: number) => void
+	onUpdate: (index: number, newText: string) => void
+}
+
+const QueuedMessages: React.FC<QueuedMessagesProps> = ({ queue, onRemove, onUpdate }) => {
+	const { t } = useTranslation("chat")
+	const [editingStates, setEditingStates] = useState<Record<string, { isEditing: boolean; value: string }>>({})
+
+	if (queue.length === 0) {
+		return null
+	}
+
+	const getEditState = (messageId: string, currentText: string) => {
+		return editingStates[messageId] || { isEditing: false, value: currentText }
+	}
+
+	const setEditState = (messageId: string, isEditing: boolean, value?: string) => {
+		setEditingStates((prev) => ({
+			...prev,
+			[messageId]: { isEditing, value: value ?? prev[messageId]?.value ?? "" },
+		}))
+	}
+
+	const handleSaveEdit = (index: number, messageId: string, newValue: string) => {
+		onUpdate(index, newValue)
+		setEditState(messageId, false)
+	}
+
+	return (
+		<div className="px-[15px] py-[10px] pr-[6px]" data-testid="queued-messages">
+			<div className="text-vscode-descriptionForeground text-md mb-2">{t("queuedMessages.title")}</div>
+			<div className="flex flex-col gap-2 max-h-[300px] overflow-y-auto pr-2">
+				{queue.map((message, index) => {
+					const editState = getEditState(message.id, message.text)
+
+					return (
+						<div
+							key={message.id}
+							className="bg-vscode-editor-background border rounded-xs p-1 overflow-hidden whitespace-pre-wrap flex-shrink-0">
+							<div className="flex justify-between">
+								<div className="flex-grow px-2 py-1 wrap-anywhere">
+									{editState.isEditing ? (
+										<textarea
+											ref={(textarea) => {
+												if (textarea) {
+													// Set cursor at the end
+													textarea.setSelectionRange(
+														textarea.value.length,
+														textarea.value.length,
+													)
+												}
+											}}
+											value={editState.value}
+											onChange={(e) => setEditState(message.id, true, e.target.value)}
+											onBlur={() => handleSaveEdit(index, message.id, editState.value)}
+											onKeyDown={(e) => {
+												if (e.key === "Enter" && !e.shiftKey) {
+													e.preventDefault()
+													handleSaveEdit(index, message.id, editState.value)
+												}
+												if (e.key === "Escape") {
+													setEditState(message.id, false, message.text)
+												}
+											}}
+											className="w-full bg-vscode-input-background text-vscode-input-foreground border border-vscode-input-border rounded px-2 py-1 resize-none focus:outline-0 focus:ring-1 focus:ring-vscode-focusBorder"
+											placeholder={t("chat:editMessage.placeholder")}
+											autoFocus
+											rows={Math.min(editState.value.split("\n").length, 10)}
+										/>
+									) : (
+										<div
+											onClick={() => setEditState(message.id, true, message.text)}
+											className="cursor-pointer hover:bg-vscode-list-hoverBackground px-1 py-0.5 -mx-1 -my-0.5 rounded transition-colors"
+											title={t("chat:queuedMessages.clickToEdit")}>
+											<Mention text={message.text} withShadow />
+										</div>
+									)}
+								</div>
+								<div className="flex">
+									<Button
+										variant="ghost"
+										size="icon"
+										className="shrink-0"
+										onClick={(e) => {
+											e.stopPropagation()
+											onRemove(index)
+										}}>
+										<span className="codicon codicon-trash" />
+									</Button>
+								</div>
+							</div>
+							{message.images && message.images.length > 0 && (
+								<Thumbnails images={message.images} style={{ marginTop: "8px" }} />
+							)}
+						</div>
+					)
+				})}
+			</div>
+		</div>
+	)
+}
+
+export default QueuedMessages

+ 2 - 2
webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx

@@ -77,7 +77,7 @@ describe("ChatTextArea", () => {
 	})
 
 	describe("enhance prompt button", () => {
-		it("should be disabled when sendingDisabled is true", () => {
+		it("should be enabled even when sendingDisabled is true (for message queueing)", () => {
 			;(useExtensionState as ReturnType<typeof vi.fn>).mockReturnValue({
 				filePaths: [],
 				openedTabs: [],
@@ -86,7 +86,7 @@ describe("ChatTextArea", () => {
 			})
 			render(<ChatTextArea {...defaultProps} sendingDisabled={true} />)
 			const enhanceButton = getEnhancePromptButton()
-			expect(enhanceButton).toHaveClass("cursor-not-allowed")
+			expect(enhanceButton).toHaveClass("cursor-pointer")
 		})
 	})
 

Разница между файлами не показана из-за своего большого размера
+ 357 - 431
webview-ui/src/components/chat/__tests__/ChatView.spec.tsx


+ 4 - 0
webview-ui/src/i18n/locales/ca/chat.json

@@ -351,5 +351,9 @@
 	},
 	"command": {
 		"triggerDescription": "Activa la comanda {{name}}"
+	},
+	"queuedMessages": {
+		"title": "Missatges en cua:",
+		"clickToEdit": "Feu clic per editar el missatge"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/de/chat.json

@@ -351,5 +351,9 @@
 	},
 	"editMessage": {
 		"placeholder": "Bearbeite deine Nachricht..."
+	},
+	"queuedMessages": {
+		"title": "Warteschlange Nachrichten:",
+		"clickToEdit": "Klicken zum Bearbeiten der Nachricht"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/en/chat.json

@@ -351,5 +351,9 @@
 	},
 	"command": {
 		"triggerDescription": "Trigger the {{name}} command"
+	},
+	"queuedMessages": {
+		"title": "Queued Messages:",
+		"clickToEdit": "Click to edit message"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/es/chat.json

@@ -351,5 +351,9 @@
 	},
 	"command": {
 		"triggerDescription": "Activar el comando {{name}}"
+	},
+	"queuedMessages": {
+		"title": "Mensajes en cola:",
+		"clickToEdit": "Haz clic para editar el mensaje"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/fr/chat.json

@@ -351,5 +351,9 @@
 	},
 	"command": {
 		"triggerDescription": "Déclencher la commande {{name}}"
+	},
+	"queuedMessages": {
+		"title": "Messages en file d'attente :",
+		"clickToEdit": "Cliquez pour modifier le message"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/hi/chat.json

@@ -351,5 +351,9 @@
 	},
 	"command": {
 		"triggerDescription": "{{name}} कमांड को ट्रिगर करें"
+	},
+	"queuedMessages": {
+		"title": "कतार में संदेश:",
+		"clickToEdit": "संदेश संपादित करने के लिए क्लिक करें"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/id/chat.json

@@ -357,5 +357,9 @@
 	},
 	"command": {
 		"triggerDescription": "Jalankan perintah {{name}}"
+	},
+	"queuedMessages": {
+		"title": "Pesan Antrian:",
+		"clickToEdit": "Klik untuk mengedit pesan"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/it/chat.json

@@ -351,5 +351,9 @@
 	},
 	"command": {
 		"triggerDescription": "Attiva il comando {{name}}"
+	},
+	"queuedMessages": {
+		"title": "Messaggi in coda:",
+		"clickToEdit": "Clicca per modificare il messaggio"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/ja/chat.json

@@ -351,5 +351,9 @@
 	},
 	"command": {
 		"triggerDescription": "{{name}}コマンドをトリガー"
+	},
+	"queuedMessages": {
+		"title": "キューメッセージ:",
+		"clickToEdit": "クリックしてメッセージを編集"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/ko/chat.json

@@ -351,5 +351,9 @@
 	},
 	"command": {
 		"triggerDescription": "{{name}} 명령 트리거"
+	},
+	"queuedMessages": {
+		"title": "대기열 메시지:",
+		"clickToEdit": "클릭하여 메시지 편집"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/nl/chat.json

@@ -351,5 +351,9 @@
 	},
 	"command": {
 		"triggerDescription": "Activeer de {{name}} opdracht"
+	},
+	"queuedMessages": {
+		"title": "Berichten in wachtrij:",
+		"clickToEdit": "Klik om bericht te bewerken"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/pl/chat.json

@@ -351,5 +351,9 @@
 	},
 	"command": {
 		"triggerDescription": "Uruchom polecenie {{name}}"
+	},
+	"queuedMessages": {
+		"title": "Wiadomości w kolejce:",
+		"clickToEdit": "Kliknij, aby edytować wiadomość"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/pt-BR/chat.json

@@ -351,5 +351,9 @@
 	},
 	"command": {
 		"triggerDescription": "Acionar o comando {{name}}"
+	},
+	"queuedMessages": {
+		"title": "Mensagens na fila:",
+		"clickToEdit": "Clique para editar a mensagem"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/ru/chat.json

@@ -351,5 +351,9 @@
 	},
 	"command": {
 		"triggerDescription": "Запустить команду {{name}}"
+	},
+	"queuedMessages": {
+		"title": "Сообщения в очереди:",
+		"clickToEdit": "Нажмите, чтобы редактировать сообщение"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/tr/chat.json

@@ -351,5 +351,9 @@
 	},
 	"command": {
 		"triggerDescription": "{{name}} komutunu tetikle"
+	},
+	"queuedMessages": {
+		"title": "Sıradaki Mesajlar:",
+		"clickToEdit": "Mesajı düzenlemek için tıkla"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/vi/chat.json

@@ -351,5 +351,9 @@
 	},
 	"command": {
 		"triggerDescription": "Kích hoạt lệnh {{name}}"
+	},
+	"queuedMessages": {
+		"title": "Tin nhắn trong hàng đợi:",
+		"clickToEdit": "Nhấp để chỉnh sửa tin nhắn"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/zh-CN/chat.json

@@ -351,5 +351,9 @@
 	},
 	"editMessage": {
 		"placeholder": "编辑消息..."
+	},
+	"queuedMessages": {
+		"title": "队列消息:",
+		"clickToEdit": "点击编辑消息"
 	}
 }

+ 4 - 0
webview-ui/src/i18n/locales/zh-TW/chat.json

@@ -351,5 +351,9 @@
 	},
 	"command": {
 		"triggerDescription": "觸發 {{name}} 命令"
+	},
+	"queuedMessages": {
+		"title": "佇列訊息:",
+		"clickToEdit": "點擊編輯訊息"
 	}
 }

Некоторые файлы не были показаны из-за большого количества измененных файлов