From 91b4d05fb7eb5417d36a69db919f9a25f3440f0f Mon Sep 17 00:00:00 2001 From: Hank Zhang Date: Wed, 17 Jun 2026 11:45:55 +0800 Subject: [PATCH] feat: add regenerate button to re-send last user message Add a 'Regenerate' button below the last agent response so users can re-run their last prompt without retyping. Changes: - MessageList: add onRegenerate prop + RegenerateBar component with RefreshCw icon, shown when agent has finished and a user message exists - Chat: add handleRegenerate that finds the last user message and re-sends it via handleSendRef - main.css: .chat-regenerate-bar + .chat-regenerate-btn styles - i18n: add copyMessage and regenerate strings to all 10 locales --- src/renderer/src/assets/main.css | 30 +++++++++++++++++ src/renderer/src/screens/Chat/Chat.tsx | 12 +++++++ src/renderer/src/screens/Chat/MessageList.tsx | 33 ++++++++++++++++++- src/shared/i18n/locales/en/chat.ts | 2 ++ src/shared/i18n/locales/es/chat.ts | 2 ++ src/shared/i18n/locales/id/chat.ts | 2 ++ src/shared/i18n/locales/ja/chat.ts | 2 ++ src/shared/i18n/locales/pl/chat.ts | 2 ++ src/shared/i18n/locales/pt-BR/chat.ts | 2 ++ src/shared/i18n/locales/pt-PT/chat.ts | 2 ++ src/shared/i18n/locales/tr/chat.ts | 2 ++ src/shared/i18n/locales/zh-CN/chat.ts | 2 ++ src/shared/i18n/locales/zh-TW/chat.ts | 2 ++ 13 files changed, 94 insertions(+), 1 deletion(-) diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index a1585ad6a..194aeebb0 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -3036,6 +3036,36 @@ body { background: var(--bg-hover); } +/* Regenerate button — thin bar below the last agent response */ +.chat-regenerate-bar { + display: flex; + justify-content: flex-start; + margin-left: 42px; + margin-top: 6px; + margin-bottom: 4px; +} + +.chat-regenerate-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + background: none; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text-secondary); + font-size: 12px; + font-family: var(--font-sans); + cursor: pointer; + transition: all var(--transition); +} + +.chat-regenerate-btn:hover { + color: var(--text-primary); + border-color: var(--border-bright); + background: var(--bg-tertiary); +} + /* Inline clarify card — a mid-turn question rendered in the transcript. */ .chat-clarify { margin-top: 8px; diff --git a/src/renderer/src/screens/Chat/Chat.tsx b/src/renderer/src/screens/Chat/Chat.tsx index eed0a00b4..e64d45e8f 100644 --- a/src/renderer/src/screens/Chat/Chat.tsx +++ b/src/renderer/src/screens/Chat/Chat.tsx @@ -468,6 +468,17 @@ function Chat({ chatInputRef.current?.setText(text); }, []); + // Regenerate: re-send the last user message to get a new agent response + const handleRegenerate = useCallback(() => { + const lastUser = [...messages] + .reverse() + .find((m) => m.role === "user") as + | import("./types").ChatBubbleMessage + | undefined; + if (!lastUser?.content) return; + void handleSendRef.current(lastUser.content, lastUser.attachments || []); + }, [messages]); + const handlePickFolder = useCallback(async () => { const path = await window.hermesAPI.selectFolder(); if (path) setContextFolder(path); @@ -559,6 +570,7 @@ function Chat({ onApprove={actions.handleApprove} onDeny={actions.handleDeny} onClarifyResolved={handleClarifyResolved} + onRegenerate={handleRegenerate} /> )}
diff --git a/src/renderer/src/screens/Chat/MessageList.tsx b/src/renderer/src/screens/Chat/MessageList.tsx index da4e15443..0fa774c28 100644 --- a/src/renderer/src/screens/Chat/MessageList.tsx +++ b/src/renderer/src/screens/Chat/MessageList.tsx @@ -1,7 +1,9 @@ import { memo, useMemo } from "react"; +import { RefreshCw } from "lucide-react"; import { HermesAvatar, MessageRow } from "./MessageRow"; import { ReasoningRow, ToolActivityGroup } from "./HistoryRow"; import { ClarifyCard } from "./ClarifyCard"; +import { useI18n } from "../../components/useI18n"; import type { ChatMessage, ClarifyMessage, @@ -22,6 +24,8 @@ interface MessageListProps { onDeny: () => void; /** Mark an inline clarify card resolved once the user answers/skips. */ onClarifyResolved: (requestId: string, answer: string) => void; + /** Called when the user clicks the regenerate button below the last agent response. */ + onRegenerate?: () => void; } function TypingIndicator({ @@ -59,6 +63,23 @@ function isBubble(m: ChatMessage): m is import("./types").ChatBubbleMessage { return !k || k === "user" || k === "assistant"; } +function RegenerateBar({ onClick }: { onClick: () => void }): React.JSX.Element { + const { t } = useI18n(); + return ( +
+ +
+ ); +} + export const MessageList = memo(function MessageList({ messages, isLoading, @@ -66,6 +87,7 @@ export const MessageList = memo(function MessageList({ onApprove, onDeny, onClarifyResolved, + onRegenerate, }: MessageListProps): React.JSX.Element { // Bubbles with empty content are still hidden (live-stream placeholders). // History rows pass through unconditionally. @@ -81,6 +103,13 @@ export const MessageList = memo(function MessageList({ const lastBubble = [...messages].reverse().find(isBubble); const lastMessageIsAgent = !!lastBubble && lastBubble.role === "agent"; + // Show regenerate: not loading, last message is from agent, has at least one user message + const hasUserMessage = messages.some( + (m) => isBubble(m) && m.role === "user", + ); + const showRegenerate = + onRegenerate && !isLoading && lastMessageIsAgent && hasUserMessage; + // Render plan: bubble/reasoning rows pass through one-to-one, but a // contiguous run of tool_call/tool_result rows folds into a single // ToolActivityGroup (collapsed by default) instead of one bubble per call. @@ -162,6 +191,8 @@ export const MessageList = memo(function MessageList({ <> {rows} + {showRegenerate && } + {isLoading && !lastMessageIsAgent && ( )} @@ -171,4 +202,4 @@ export const MessageList = memo(function MessageList({ )} ); -}); +}); \ No newline at end of file diff --git a/src/shared/i18n/locales/en/chat.ts b/src/shared/i18n/locales/en/chat.ts index 1a28eae43..b20c0aec3 100644 --- a/src/shared/i18n/locales/en/chat.ts +++ b/src/shared/i18n/locales/en/chat.ts @@ -132,6 +132,8 @@ export default { queuedCount: "{{count}} queued", queuedAttachment: "{{count}} attachment(s)", queuedCancel: "Remove from queue", + copyMessage: "Copy message", + regenerate: "Regenerate", worktree: { loading: "Loading", empty: "Folder is empty", diff --git a/src/shared/i18n/locales/es/chat.ts b/src/shared/i18n/locales/es/chat.ts index 1cdf05413..5480ee270 100644 --- a/src/shared/i18n/locales/es/chat.ts +++ b/src/shared/i18n/locales/es/chat.ts @@ -108,6 +108,8 @@ export default { }, queued: "{{count}} mensaje(s) en cola — se enviará cuando el agente termine", queuedCancel: "Quitar de la cola", + copyMessage: "Copiar mensaje", + regenerate: "Regenerar", worktree: { loading: "Cargando", empty: "La carpeta está vacía", diff --git a/src/shared/i18n/locales/id/chat.ts b/src/shared/i18n/locales/id/chat.ts index bf4a24061..75c026c00 100644 --- a/src/shared/i18n/locales/id/chat.ts +++ b/src/shared/i18n/locales/id/chat.ts @@ -82,6 +82,8 @@ export default { version: "Tampilkan versi Hermes", }, queuedCancel: "Batalkan pesan antrian", + copyMessage: "Salin pesan", + regenerate: "Buat ulang", worktree: { loading: "Memuat", empty: "Folder kosong", diff --git a/src/shared/i18n/locales/ja/chat.ts b/src/shared/i18n/locales/ja/chat.ts index 83461d9f9..cc0821a1e 100644 --- a/src/shared/i18n/locales/ja/chat.ts +++ b/src/shared/i18n/locales/ja/chat.ts @@ -81,6 +81,8 @@ export default { version: "Hermes バージョンを表示", }, queuedCancel: "キューから削除", + copyMessage: "メッセージをコピー", + regenerate: "再生成", worktree: { loading: "読み込み中", empty: "フォルダは空です", diff --git a/src/shared/i18n/locales/pl/chat.ts b/src/shared/i18n/locales/pl/chat.ts index fa5b153c1..43ea38105 100644 --- a/src/shared/i18n/locales/pl/chat.ts +++ b/src/shared/i18n/locales/pl/chat.ts @@ -88,6 +88,8 @@ export default { queued: "{{count}} wiadomość/wiadomości w kolejce — zostaną wysłane po zakończeniu pracy agenta", queuedCancel: "Usuń z kolejki", + copyMessage: "Kopiuj wiadomość", + regenerate: "Wygeneruj ponownie", worktree: { loading: "Ładowanie", empty: "Folder jest pusty", diff --git a/src/shared/i18n/locales/pt-BR/chat.ts b/src/shared/i18n/locales/pt-BR/chat.ts index 1050f2767..0702c929b 100644 --- a/src/shared/i18n/locales/pt-BR/chat.ts +++ b/src/shared/i18n/locales/pt-BR/chat.ts @@ -82,6 +82,8 @@ export default { version: "Mostrar a versão do Hermes", }, queuedCancel: "Remover da fila", + copyMessage: "Copiar mensagem", + regenerate: "Regenerar", worktree: { loading: "Carregando", empty: "A pasta está vazia", diff --git a/src/shared/i18n/locales/pt-PT/chat.ts b/src/shared/i18n/locales/pt-PT/chat.ts index 819dbc9bf..55acf31e4 100644 --- a/src/shared/i18n/locales/pt-PT/chat.ts +++ b/src/shared/i18n/locales/pt-PT/chat.ts @@ -94,6 +94,8 @@ export default { queued: "{{count}} mensagem(ns) em fila — serão enviadas quando o agente terminar", queuedCancel: "Remover da fila", + copyMessage: "Copiar mensagem", + regenerate: "Regenerar", worktree: { loading: "A carregar", empty: "A pasta está vazia", diff --git a/src/shared/i18n/locales/tr/chat.ts b/src/shared/i18n/locales/tr/chat.ts index 40c29f531..84e7b6be5 100644 --- a/src/shared/i18n/locales/tr/chat.ts +++ b/src/shared/i18n/locales/tr/chat.ts @@ -105,6 +105,8 @@ export default { }, queued: "{{count}} mesaj sırada — ajan işini bitirince gönderilecek", queuedCancel: "Sıradan kaldır", + copyMessage: "Mesajı kopyala", + regenerate: "Yeniden oluştur", worktree: { loading: "Yükleniyor", empty: "Klasör boş", diff --git a/src/shared/i18n/locales/zh-CN/chat.ts b/src/shared/i18n/locales/zh-CN/chat.ts index 49d107459..76492531c 100644 --- a/src/shared/i18n/locales/zh-CN/chat.ts +++ b/src/shared/i18n/locales/zh-CN/chat.ts @@ -78,6 +78,8 @@ export default { version: "查看 Hermes 版本", }, queuedCancel: "从队列中移除", + copyMessage: "复制消息", + regenerate: "重新生成", worktree: { loading: "加载中", empty: "文件夹为空", diff --git a/src/shared/i18n/locales/zh-TW/chat.ts b/src/shared/i18n/locales/zh-TW/chat.ts index 337c15a67..904de332b 100644 --- a/src/shared/i18n/locales/zh-TW/chat.ts +++ b/src/shared/i18n/locales/zh-TW/chat.ts @@ -67,6 +67,8 @@ export default { }, queued: "{{count}} 則訊息已排隊 — 代理完成後將自動傳送", queuedCancel: "從佇列中移除", + copyMessage: "複製訊息", + regenerate: "重新生成", worktree: { loading: "載入中", empty: "資料夾是空的",