Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions src/renderer/src/assets/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions src/renderer/src/screens/Chat/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Comment on lines +472 to +480

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Regenerate duplicates the last user message

handleRegenerate calls handleSendRef.current(...), which internally calls pushUser(text, "user", attachments) — that appends a brand-new user bubble to the messages array. As a result, every regenerate produces an extra copy of the original user message in the transcript: the conversation grows as […, user: "hello", agent: "resp1", user: "hello" (duplicate), agent: "resp2"].

The standard "regenerate" pattern strips the last agent turn (and optionally the duplicate user turn) before re-sending. At a minimum, handleSend should not push another user bubble when regenerating — consider passing sendToAgent directly, or adding a noUserBubble flag to handleSend/handleSendRef.


const handlePickFolder = useCallback(async () => {
const path = await window.hermesAPI.selectFolder();
if (path) setContextFolder(path);
Expand Down Expand Up @@ -559,6 +570,7 @@ function Chat({
onApprove={actions.handleApprove}
onDeny={actions.handleDeny}
onClarifyResolved={handleClarifyResolved}
onRegenerate={handleRegenerate}
/>
)}
<div ref={bottomRef} />
Expand Down
33 changes: 32 additions & 1 deletion src/renderer/src/screens/Chat/MessageList.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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({
Expand Down Expand Up @@ -59,13 +63,31 @@ 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 (
<div className="chat-regenerate-bar">
<button
type="button"
className="chat-regenerate-btn"
onClick={onClick}
title={t("chat.regenerate")}
>
<RefreshCw size={14} />
<span>{t("chat.regenerate")}</span>
</button>
</div>
);
}

export const MessageList = memo(function MessageList({
messages,
isLoading,
toolProgress,
onApprove,
onDeny,
onClarifyResolved,
onRegenerate,
}: MessageListProps): React.JSX.Element {
// Bubbles with empty content are still hidden (live-stream placeholders).
// History rows pass through unconditionally.
Expand All @@ -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.
Expand Down Expand Up @@ -162,6 +191,8 @@ export const MessageList = memo(function MessageList({
<>
{rows}

{showRegenerate && <RegenerateBar onClick={onRegenerate} />}

{isLoading && !lastMessageIsAgent && (
<TypingIndicator toolProgress={toolProgress} />
)}
Expand All @@ -171,4 +202,4 @@ export const MessageList = memo(function MessageList({
)}
</>
);
});
});
Comment on lines 204 to +205

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing newline at end of file. The original file ended with a newline after });; this edit dropped it.

Suggested change
);
});
});
);
});

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

2 changes: 2 additions & 0 deletions src/shared/i18n/locales/en/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ export default {
queuedCount: "{{count}} queued",
queuedAttachment: "{{count}} attachment(s)",
queuedCancel: "Remove from queue",
copyMessage: "Copy message",
regenerate: "Regenerate",
Comment on lines +135 to +136

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The copyMessage key is added here and to all 10 locale files, but there is no reference to t("chat.copyMessage") anywhere in the PR. Shipping unused i18n keys adds translation overhead without benefit; consider removing it if the copy-message feature is not yet implemented.

Suggested change
copyMessage: "Copy message",
regenerate: "Regenerate",
regenerate: "Regenerate",

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

worktree: {
loading: "Loading",
empty: "Folder is empty",
Expand Down
2 changes: 2 additions & 0 deletions src/shared/i18n/locales/es/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/shared/i18n/locales/id/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/shared/i18n/locales/ja/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export default {
version: "Hermes バージョンを表示",
},
queuedCancel: "キューから削除",
copyMessage: "メッセージをコピー",
regenerate: "再生成",
worktree: {
loading: "読み込み中",
empty: "フォルダは空です",
Expand Down
2 changes: 2 additions & 0 deletions src/shared/i18n/locales/pl/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/shared/i18n/locales/pt-BR/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/shared/i18n/locales/pt-PT/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions src/shared/i18n/locales/tr/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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ş",
Expand Down
2 changes: 2 additions & 0 deletions src/shared/i18n/locales/zh-CN/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export default {
version: "查看 Hermes 版本",
},
queuedCancel: "从队列中移除",
copyMessage: "复制消息",
regenerate: "重新生成",
worktree: {
loading: "加载中",
empty: "文件夹为空",
Expand Down
2 changes: 2 additions & 0 deletions src/shared/i18n/locales/zh-TW/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ export default {
},
queued: "{{count}} 則訊息已排隊 — 代理完成後將自動傳送",
queuedCancel: "從佇列中移除",
copyMessage: "複製訊息",
regenerate: "重新生成",
worktree: {
loading: "載入中",
empty: "資料夾是空的",
Expand Down
Loading