-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat: add regenerate button to re‑send last prompt #700
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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, | ||
|
|
@@ -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,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. | ||
|
|
@@ -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 && <RegenerateBar onClick={onRegenerate} />} | ||
|
|
||
| {isLoading && !lastMessageIsAgent && ( | ||
| <TypingIndicator toolProgress={toolProgress} /> | ||
| )} | ||
|
|
@@ -171,4 +202,4 @@ export const MessageList = memo(function MessageList({ | |
| )} | ||
| </> | ||
| ); | ||
| }); | ||
| }); | ||
|
Comment on lines
204
to
+205
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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! |
||||||||
| worktree: { | ||||||||
| loading: "Loading", | ||||||||
| empty: "Folder is empty", | ||||||||
|
|
||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
handleRegeneratecallshandleSendRef.current(...), which internally callspushUser(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,
handleSendshould not push another user bubble when regenerating — consider passingsendToAgentdirectly, or adding anoUserBubbleflag tohandleSend/handleSendRef.