ux: Standard stop button 🟥 (#10639)

Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com>
This commit is contained in:
Bruno Bergher 2026-01-13 06:25:17 +00:00 committed by GitHub
parent b514996208
commit a12163d762
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 210 additions and 54 deletions

View file

@ -1,7 +1,7 @@
import React, { forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"
import { useEvent } from "react-use"
import DynamicTextArea from "react-textarea-autosize"
import { VolumeX, Image, WandSparkles, SendHorizontal, MessageSquareX } from "lucide-react"
import { VolumeX, Image, WandSparkles, SendHorizontal, MessageSquareX, ListEnd, Square } from "lucide-react"
import type { ExtensionMessage } from "@roo-code/types"
@ -55,6 +55,10 @@ interface ChatTextAreaProps {
// Browser session status
isBrowserSessionActive?: boolean
showBrowserDockToggle?: boolean
// Stop/Queue functionality
isStreaming?: boolean
onStop?: () => void
onEnqueueMessage?: () => void
}
export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
@ -77,6 +81,9 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
onCancel,
isBrowserSessionActive = false,
showBrowserDockToggle = false,
isStreaming = false,
onStop,
onEnqueueMessage,
},
ref,
) => {
@ -1185,30 +1192,68 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
</button>
</StandardTooltip>
)}
<StandardTooltip
content={t("chat:pressToSend", { keyCombination: sendKeyCombination })}>
<button
aria-label={t("chat:pressToSend", { keyCombination: sendKeyCombination })}
disabled={false}
onClick={onSend}
className={cn(
"relative inline-flex items-center justify-center",
"bg-transparent border-none p-1.5",
"rounded-md min-w-[28px] min-h-[28px]",
"text-vscode-descriptionForeground hover:text-vscode-foreground",
"transition-all duration-200",
hasInputContent
? "opacity-100 hover:opacity-100 pointer-events-auto"
: "opacity-0 pointer-events-none",
hasInputContent &&
{/* Queue button - shown when streaming and user has typed content */}
{!isEditMode && isStreaming && hasInputContent && onEnqueueMessage && (
<StandardTooltip content={t("chat:enqueueMessage")}>
<button
aria-label={t("chat:enqueueMessage")}
disabled={false}
onClick={onEnqueueMessage}
className={cn(
"relative inline-flex items-center justify-center",
"bg-transparent border-none p-1.5",
"rounded-md min-w-[28px] min-h-[28px]",
"text-vscode-descriptionForeground hover:text-vscode-foreground",
"transition-all duration-200",
"opacity-100 hover:opacity-100 pointer-events-auto",
"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",
hasInputContent && "active:bg-[rgba(255,255,255,0.1)]",
hasInputContent && "cursor-pointer",
)}>
<SendHorizontal className="w-4 h-4" />
</button>
</StandardTooltip>
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
"active:bg-[rgba(255,255,255,0.1)]",
"cursor-pointer",
)}>
<ListEnd className="w-4 h-4" />
</button>
</StandardTooltip>
)}
{/* Send/Stop button - morphs based on streaming state */}
{!isEditMode && (
<StandardTooltip
content={
isStreaming
? t("chat:stop.title")
: t("chat:pressToSend", { keyCombination: sendKeyCombination })
}>
<button
aria-label={
isStreaming
? t("chat:stop.title")
: t("chat:pressToSend", { keyCombination: sendKeyCombination })
}
disabled={false}
onClick={isStreaming ? onStop : onSend}
className={cn(
"relative inline-flex items-center justify-center",
"bg-transparent border-none p-1.5",
"rounded-md min-w-[28px] min-h-[28px]",
"text-vscode-descriptionForeground hover:text-vscode-foreground",
"transition-all duration-200",
isStreaming || hasInputContent
? "opacity-100 hover:opacity-100 pointer-events-auto"
: "opacity-0 pointer-events-none",
(isStreaming || hasInputContent) &&
"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",
(isStreaming || hasInputContent) && "active:bg-[rgba(255,255,255,0.1)]",
(isStreaming || hasInputContent) && "cursor-pointer",
)}>
{isStreaming ? (
<Square className="size-4 fill-vscode-descriptionForeground" />
) : (
<SendHorizontal className="size-4" />
)}
</button>
</StandardTooltip>
)}
</div>
{!inputValue && (

View file

@ -142,7 +142,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
const [enableButtons, setEnableButtons] = useState<boolean>(false)
const [primaryButtonText, setPrimaryButtonText] = useState<string | undefined>(undefined)
const [secondaryButtonText, setSecondaryButtonText] = useState<string | undefined>(undefined)
const [didClickCancel, setDidClickCancel] = useState(false)
const [_didClickCancel, setDidClickCancel] = useState(false)
const virtuosoRef = useRef<VirtuosoHandle>(null)
const [expandedRows, setExpandedRows] = useState<Record<number, boolean>>({})
const prevExpandedRowsRef = useRef<Record<number, boolean>>()
@ -661,6 +661,26 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
const startNewTask = useCallback(() => vscode.postMessage({ type: "clearTask" }), [])
// Handle stop button click from textarea
const handleStopTask = useCallback(() => {
vscode.postMessage({ type: "cancelTask" })
setDidClickCancel(true)
}, [setDidClickCancel])
// Handle enqueue button click from textarea
const handleEnqueueCurrentMessage = useCallback(() => {
const text = inputValue.trim()
if (text || selectedImages.length > 0) {
vscode.postMessage({
type: "queueMessage",
text,
images: selectedImages,
})
setInputValue("")
setSelectedImages([])
}
}, [inputValue, selectedImages])
// This logic depends on the useEffect[messages] above to set clineAsk,
// after which buttons are shown and we then send an askResponse to the
// extension.
@ -733,6 +753,8 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
setSendingDisabled(true)
setClineAsk(undefined)
setEnableButtons(false)
setPrimaryButtonText(undefined)
setSecondaryButtonText(undefined)
},
[clineAsk, startNewTask, currentTaskItem?.parentTaskId],
)
@ -784,7 +806,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
setClineAsk(undefined)
setEnableButtons(false)
},
[clineAsk, startNewTask, isStreaming],
[clineAsk, startNewTask, isStreaming, setDidClickCancel],
)
const { info: model } = useSelectedModel(apiConfiguration)
@ -1386,7 +1408,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
vscode.postMessage({ type: "condenseTaskContextRequest", text: taskId })
}
const areButtonsVisible = showScrollToBottom || primaryButtonText || secondaryButtonText || isStreaming
const areButtonsVisible = showScrollToBottom || primaryButtonText || secondaryButtonText
return (
<div
@ -1489,11 +1511,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
{areButtonsVisible && (
<div
className={`flex h-9 items-center mb-1 px-[15px] ${
showScrollToBottom
? "opacity-100"
: enableButtons || (isStreaming && !didClickCancel)
? "opacity-100"
: "opacity-50"
showScrollToBottom ? "opacity-100" : enableButtons ? "opacity-100" : "opacity-50"
}`}>
{showScrollToBottom ? (
<StandardTooltip content={t("chat:scrollToBottom")}>
@ -1513,7 +1531,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
</StandardTooltip>
) : (
<>
{primaryButtonText && !isStreaming && (
{primaryButtonText && (
<StandardTooltip
content={
primaryButtonText === t("chat:retry.title")
@ -1545,25 +1563,25 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
</Button>
</StandardTooltip>
)}
{(secondaryButtonText || isStreaming) && (
{secondaryButtonText && (
<StandardTooltip
content={
isStreaming
? t("chat:cancel.tooltip")
: secondaryButtonText === t("chat:startNewTask.title")
? t("chat:startNewTask.tooltip")
: secondaryButtonText === t("chat:reject.title")
? t("chat:reject.tooltip")
: secondaryButtonText === t("chat:terminate.title")
? t("chat:terminate.tooltip")
secondaryButtonText === t("chat:startNewTask.title")
? t("chat:startNewTask.tooltip")
: secondaryButtonText === t("chat:reject.title")
? t("chat:reject.tooltip")
: secondaryButtonText === t("chat:terminate.title")
? t("chat:terminate.tooltip")
: secondaryButtonText === t("chat:killCommand.title")
? t("chat:killCommand.tooltip")
: undefined
}>
<Button
variant="secondary"
disabled={!enableButtons && !(isStreaming && !didClickCancel)}
className={isStreaming ? "flex-[2] ml-0" : "flex-1 ml-[6px]"}
disabled={!enableButtons}
className="flex-1 ml-[6px]"
onClick={() => handleSecondaryButtonClick(inputValue, selectedImages)}>
{isStreaming ? t("chat:cancel.title") : secondaryButtonText}
{secondaryButtonText}
</Button>
</StandardTooltip>
)}
@ -1612,6 +1630,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
modeShortcutText={modeShortcutText}
isBrowserSessionActive={!!isBrowserSessionActive}
showBrowserDockToggle={showBrowserDockToggle}
isStreaming={isStreaming}
onStop={handleStopTask}
onEnqueueMessage={handleEnqueueCurrentMessage}
/>
{isProfileDisabled && (

View file

@ -82,10 +82,15 @@
"title": "Finalitzar",
"tooltip": "Finalitza la tasca actual"
},
"stop": {
"title": "Atura",
"tooltip": "Atura la tasca actual"
},
"cancel": {
"title": "Cancel·lar",
"tooltip": "Cancel·la l'operació actual"
},
"enqueueMessage": "Afegeix el missatge a la cua (s'enviarà quan acabi la tasca actual)",
"scrollToBottom": "Desplaça't al final del xat",
"about": "Roo Code és tot un equip de desenvolupament d'IA al teu editor.",
"docs": "Consulta els nostres <DocsLink>documents</DocsLink> per a més informació.",

View file

@ -82,10 +82,15 @@
"title": "Beenden",
"tooltip": "Aktuelle Aufgabe beenden"
},
"stop": {
"title": "Stoppen",
"tooltip": "Aktuelle Aufgabe stoppen"
},
"cancel": {
"title": "Abbrechen",
"tooltip": "Aktuelle Operation abbrechen"
},
"enqueueMessage": "Nachricht zur Warteschlange hinzufügen (wird nach Abschluss der aktuellen Aufgabe gesendet)",
"scrollToBottom": "Zum Chat-Ende scrollen",
"about": "Roo Code ist ein ganzes KI-Entwicklerteam in deinem Editor.",
"docs": "Schau in unsere <DocsLink>Dokumentation</DocsLink>, um mehr zu erfahren.",

View file

@ -54,13 +54,13 @@
"reservedForResponse": "Reserved for model response: {{amount}} tokens"
},
"reject": {
"title": "Reject",
"tooltip": "Reject this action"
"title": "Deny",
"tooltip": "Prevent this action from occurring"
},
"completeSubtaskAndReturn": "Complete Subtask and Return",
"approve": {
"title": "Approve",
"tooltip": "Approve this action"
"tooltip": "Allow this action to happen"
},
"read-batch": {
"approve": {
@ -75,25 +75,30 @@
"tooltip": "Execute this command"
},
"proceedWhileRunning": {
"title": "Proceed While Running",
"tooltip": "Continue despite warnings"
"title": "Continue While Running",
"tooltip": "Keep going despite warnings"
},
"killCommand": {
"title": "Kill Command",
"tooltip": "Kill the current command"
},
"resumeTask": {
"title": "Resume Task",
"tooltip": "Continue the current task"
"title": "Continue",
"tooltip": "Resume the current task"
},
"terminate": {
"title": "Terminate",
"tooltip": "End the current task"
"title": "New task",
"tooltip": "Start a new task"
},
"cancel": {
"title": "Cancel",
"tooltip": "Cancel the current operation"
},
"stop": {
"title": "Stop",
"tooltip": "Stop the current task"
},
"enqueueMessage": "Add message to queue (will be sent after current task completes)",
"editMessage": {
"placeholder": "Edit your message..."
},

View file

@ -82,10 +82,15 @@
"title": "Terminar",
"tooltip": "Terminar la tarea actual"
},
"stop": {
"title": "Detener",
"tooltip": "Detener la tarea actual"
},
"cancel": {
"title": "Cancelar",
"tooltip": "Cancelar la operación actual"
},
"enqueueMessage": "Agregar mensaje a la cola (se enviará después de que termine la tarea actual)",
"scrollToBottom": "Desplazarse al final del chat",
"about": "Roo Code es todo un equipo de desarrollo de IA en tu editor.",
"docs": "Consulta nuestra <DocsLink>documentación</DocsLink> para saber más.",

View file

@ -82,10 +82,15 @@
"title": "Terminer",
"tooltip": "Terminer la tâche actuelle"
},
"stop": {
"title": "Arrêter",
"tooltip": "Arrêter la tâche en cours"
},
"cancel": {
"title": "Annuler",
"tooltip": "Annuler l'opération actuelle"
},
"enqueueMessage": "Ajouter le message à la file d'attente (sera envoyé après la fin de la tâche en cours)",
"scrollToBottom": "Défiler jusqu'au bas du chat",
"about": "Roo Code est une équipe complète de développeurs IA dans votre éditeur.",
"docs": "Consultez notre <DocsLink>documentation</DocsLink> pour en savoir plus.",

View file

@ -82,10 +82,15 @@
"title": "समाप्त करें",
"tooltip": "वर्तमान कार्य समाप्त करें"
},
"stop": {
"title": "रोकें",
"tooltip": "वर्तमान कार्य रोकें"
},
"cancel": {
"title": "रद्द करें",
"tooltip": "वर्तमान ऑपरेशन रद्द करें"
},
"enqueueMessage": "संदेश को कतार में जोड़ें (वर्तमान कार्य पूरा होने के बाद भेजा जाएगा)",
"scrollToBottom": "चैट के निचले हिस्से तक स्क्रॉल करें",
"about": "Roo Code आपके संपादक में एक पूरी AI देव टीम है।",
"docs": "और जानने के लिए हमारे <DocsLink>दस्तावेज़</DocsLink> देखें।",

View file

@ -96,10 +96,15 @@
"title": "Hentikan",
"tooltip": "Akhiri tugas saat ini"
},
"stop": {
"title": "Stop",
"tooltip": "Hentikan tugas saat ini"
},
"cancel": {
"title": "Batal",
"tooltip": "Batalkan operasi saat ini"
},
"enqueueMessage": "Tambahkan pesan ke antrean (akan dikirim setelah tugas saat ini selesai)",
"scrollToBottom": "Gulir ke bawah chat",
"about": "Roo Code adalah seluruh tim pengembang AI di editor Anda.",
"docs": "Lihat <DocsLink>dokumentasi</DocsLink> kami untuk mempelajari lebih lanjut.",

View file

@ -82,10 +82,15 @@
"title": "Termina",
"tooltip": "Termina l'attività corrente"
},
"stop": {
"title": "Ferma",
"tooltip": "Ferma l'attività corrente"
},
"cancel": {
"title": "Annulla",
"tooltip": "Annulla l'operazione corrente"
},
"enqueueMessage": "Aggiungi il messaggio alla coda (sarà inviato dopo che l'attività corrente sarà terminata)",
"scrollToBottom": "Scorri fino alla fine della chat",
"about": "Roo Code è un intero team di sviluppo AI nel tuo editor.",
"docs": "Consulta la nostra <DocsLink>documentazione</DocsLink> per saperne di più.",

View file

@ -82,10 +82,15 @@
"title": "終了",
"tooltip": "現在のタスクを終了"
},
"stop": {
"title": "停止",
"tooltip": "現在のタスクを停止"
},
"cancel": {
"title": "キャンセル",
"tooltip": "現在の操作をキャンセル"
},
"enqueueMessage": "メッセージをキューに追加(現在のタスク完了後に送信されます)",
"scrollToBottom": "チャットの最下部にスクロール",
"about": "Roo Codeは、エディタに常駐するAI開発チームです。",
"docs": "詳細については、<DocsLink>ドキュメント</DocsLink>をご確認ください。",

View file

@ -82,10 +82,15 @@
"title": "종료",
"tooltip": "현재 작업 종료"
},
"stop": {
"title": "정지",
"tooltip": "현재 작업 정지"
},
"cancel": {
"title": "취소",
"tooltip": "현재 작업 취소"
},
"enqueueMessage": "메시지를 대기열에 추가 (현재 작업 완료 후 전송)",
"scrollToBottom": "채팅 하단으로 스크롤",
"about": "Roo Code는 편집기 안에 있는 전체 AI 개발팀입니다.",
"docs": "더 알아보려면 <DocsLink>문서</DocsLink>를 확인하세요.",

View file

@ -82,10 +82,15 @@
"title": "Beëindigen",
"tooltip": "Beëindig de huidige taak"
},
"stop": {
"title": "Stoppen",
"tooltip": "Stop de huidige taak"
},
"cancel": {
"title": "Annuleren",
"tooltip": "Annuleer de huidige bewerking"
},
"enqueueMessage": "Bericht aan de wachtrij toevoegen (wordt verzonden nadat de huidige taak is voltooid)",
"scrollToBottom": "Scroll naar onderaan de chat",
"about": "Roo Code is een heel AI-ontwikkelteam in je editor.",
"docs": "Bekijk onze <DocsLink>documentatie</DocsLink> voor meer informatie.",

View file

@ -82,10 +82,15 @@
"title": "Zakończ",
"tooltip": "Zakończ bieżące zadanie"
},
"stop": {
"title": "Zatrzymaj",
"tooltip": "Zatrzymaj bieżące zadanie"
},
"cancel": {
"title": "Anuluj",
"tooltip": "Anuluj bieżącą operację"
},
"enqueueMessage": "Dodaj wiadomość do kolejki (zostanie wysłana po zakończeniu bieżącego zadania)",
"scrollToBottom": "Przewiń do dołu czatu",
"about": "Roo Code to cały zespół deweloperów AI w Twoim edytorze.",
"docs": "Sprawdź naszą <DocsLink>dokumentację</DocsLink>, aby dowiedzieć się więcej.",

View file

@ -82,10 +82,15 @@
"title": "Terminar",
"tooltip": "Encerrar a tarefa atual"
},
"stop": {
"title": "Parar",
"tooltip": "Parar a tarefa atual"
},
"cancel": {
"title": "Cancelar",
"tooltip": "Cancelar a operação atual"
},
"enqueueMessage": "Adicionar mensagem à fila (será enviada após a conclusão da tarefa atual)",
"scrollToBottom": "Rolar para o final do chat",
"about": "Roo Code é uma equipe inteira de desenvolvimento de IA em seu editor.",
"docs": "Confira nossa <DocsLink>documentação</DocsLink> para saber mais.",

View file

@ -82,10 +82,15 @@
"title": "Завершить",
"tooltip": "Завершить текущую задачу"
},
"stop": {
"title": "Остановить",
"tooltip": "Остановить текущую задачу"
},
"cancel": {
"title": "Отмена",
"tooltip": "Отменить текущую операцию"
},
"enqueueMessage": "Добавить сообщение в очередь (будет отправлено после завершения текущей задачи)",
"scrollToBottom": "Прокрутить чат вниз",
"about": "Roo Code — это целая команда разработчиков ИИ в вашем редакторе.",
"docs": "Ознакомьтесь с нашей <DocsLink>документацией</DocsLink>, чтобы узнать больше.",

View file

@ -82,10 +82,15 @@
"title": "Sonlandır",
"tooltip": "Mevcut görevi sonlandır"
},
"stop": {
"title": "Durdur",
"tooltip": "Mevcut görevi durdur"
},
"cancel": {
"title": "İptal",
"tooltip": "Mevcut işlemi iptal et"
},
"enqueueMessage": "Mesajı kuyruğa ekle (mevcut görev tamamlandıktan sonra gönderilecek)",
"scrollToBottom": "Sohbetin altına kaydır",
"about": "Roo Code, düzenleyicinizdeki bütün bir yapay zeka geliştirme ekibidir.",
"docs": "Daha fazla bilgi için <DocsLink>belgelerimize</DocsLink> göz atın.",

View file

@ -82,10 +82,15 @@
"title": "Kết thúc",
"tooltip": "Kết thúc nhiệm vụ hiện tại"
},
"stop": {
"title": "Dừng",
"tooltip": "Dừng nhiệm vụ hiện tại"
},
"cancel": {
"title": "Hủy",
"tooltip": "Hủy thao tác hiện tại"
},
"enqueueMessage": "Thêm tin nhắn vào hàng đợi (sẽ gửi sau khi nhiệm vụ hiện tại hoàn tất)",
"scrollToBottom": "Cuộn xuống cuối cuộc trò chuyện",
"about": "Roo Code là một đội ngũ phát triển AI đầy đủ trong trình chỉnh sửa của bạn.",
"docs": "Kiểm tra <DocsLink>tài liệu</DocsLink> của chúng tôi để tìm hiểu thêm.",

View file

@ -86,6 +86,11 @@
"title": "取消",
"tooltip": "取消当前操作"
},
"stop": {
"title": "停止",
"tooltip": "停止当前任务"
},
"enqueueMessage": "将消息加入队列(当前任务完成后发送)",
"scrollToBottom": "滚动到聊天底部",
"about": "Roo Code 是您编辑器中的整个 AI 开发团队。",
"docs": "查看我们的 <DocsLink>文档</DocsLink> 了解更多信息。",

View file

@ -94,6 +94,11 @@
"title": "取消",
"tooltip": "取消目前操作"
},
"stop": {
"title": "停止",
"tooltip": "停止目前的工作"
},
"enqueueMessage": "將訊息加入佇列(會在目前工作完成後傳送)",
"editMessage": {
"placeholder": "編輯您的訊息..."
},