Conversation
- Add retryMessage(messageId) to useChat — truncates from the target message onward and resends the preceding user message. Works for both user and assistant messages. - Add editMessage(messageId) to useChat — truncates from the target user message onward and populates the chat input draft with the original text for revision. - Wire onRetryMessage and onEditMessage from ChatView through MessageTimeline to MessageBubble. - Remove role gate so retry icon shows on both user and assistant messages (edit remains user-only).
ChatInput initialized local text via useState(initialValue) which only reads the prop on mount. Subsequent changes (e.g. cancel edit then re-edit) were ignored. Added useEffect to re-sync setTextRaw when initialValue changes. Uses setTextRaw (not setText) to avoid redundant onDraftChange callback since the store already holds the correct value.
The focus effect only ran on mount. Now keyed on editingMessageId so the textarea receives focus whenever the user clicks edit.
Pivot from bottom-ChatInput editing to inline editing. When clicking edit, the message bubble transforms into a textarea with Save and Cancel buttons. Enter saves, Escape cancels. - MessageBubble: add isEditing mode with inline textarea, auto-resize, focus, and keyboard shortcuts (Enter=save, Escape=cancel) - MessageTimeline: pass editingMessageId, onSaveEdit, onCancelEdit - ChatView: add handleSaveEdit (truncate + send), wire to timeline - ChatInput: remove edit indicator bar, editingMessageId prop, and edit-mode focus effect
When editing, the container breaks out of the 80% user bubble constraint to span the full timeline width with a subtle bg-muted/40 background. Hides the user avatar during edit. Adds hint text indicating editing will start a new conversation.
1. handleSaveEdit now forces chatState to idle via store and defers sendMessage via pendingEditSend ref + useEffect, avoiding the stale closure where sendMessage would bail on old chatState. 2. Split focus into a separate useEffect keyed only on isEditing so it doesn't fire on every keystroke and reset cursor position. 3. Removed dead setDraft call in editMessage — inline edit reads text directly from the message, not the draft store. 4. Removed unused edit.label and edit.cancel i18n keys.
User messages render as rounded bg-muted bubbles on the content div only — action buttons and timestamp sit outside. Max width capped at 640px. Avatar removed. Outer group/gap-3 hover structure preserved.
…tions Swap the fragile CSS group-hover opacity pattern for Radix HoverCard which handles hover intent, open/close delays, and portal rendering. Actions appear below the bubble on hover via stripped HoverCardContent (no border, shadow, padding, or background). Removes group class.
Force-override base HoverCardContent styles (shadow, border, bg, animations, rounded, padding) with !important. Add mb-6 to user content div so the portal actions float in the reserved gap.
Remove !animate-none override so fade and slide transitions from the base HoverCardContent are preserved. All other chrome overrides (shadow, border, bg, padding, rounded) remain.
8 new tests: inline textarea pre-fill, save/cancel clicks, Enter/Escape keys, disabled save on empty text, edit hint visibility, retry click. Updated existing hover test to trigger HoverCard via userEvent.hover. All 49 tests green (25 MessageBubble + 24 ChatInput).
If editIndex is -1 (message deleted between click and save), bail early instead of sending to untruncated history. Clears edit state.
…cale 1. Added variant='bare' to HoverCardContent — strips all chrome. MessageBubble uses it instead of six !important overrides. 2. Removed duplicate height calc from ChatInput handleInput — useLayoutEffect([text]) already covers it. 3. Edit state (editText, editTextareaRef, effects) gated on role==='user' via canEdit flag. 4. Added edit.hint to Spanish locale.
…pped Replace py-4 with pt-4 pb-12 on the timeline inner container to give the last message enough room for HoverCard actions to render.
Bare variant now z-40 instead of z-50. Popovers and dropdowns at z-50 will always paint on top of message action hovercards.
Removed size=sm and h-7/px-3/text-xs overrides from Save/Cancel. Hint text now left-aligned, buttons right-anchored via ml-auto.
Replace px-4 py-2.5 with p-3 for consistent 12px padding on all sides.
f7e3502 to
6a5d19f
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6a5d19ff3d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…dead code Bug 1: retryMessage no longer truncates history on image-only messages. Content is validated BEFORE truncation — if there's nothing to re-send (no text, no images, no attachments), the operation bails cleanly. Bug 2: retryMessage and handleSaveEdit now forward attachments and persona from the original message. Added rebuildAttachmentDrafts() to reconstruct ChatAttachmentDraft[] from stored message content (ImageContent blocks) and metadata.attachments (file/directory refs). Dead code: Removed MessageBranch components (~210 lines) from message.tsx and messageBranch i18n keys from en/es common.json. Unused forking artifacts. Tests: +6 new tests covering image-only retry safety, attachment preservation, assistant edit-button exclusion, Shift+Enter multiline, and chatStore editingMessageId set/clear/cleanup. 430 tests green.
6a5d19f to
5374dfb
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5374dfbc92
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…rrow TS union Blocker 1: Narrow ChatAttachmentDraft union in attachments.test.ts before asserting base64 — fixes TS compilation error that broke CI. Blocker 2: Unify pendingEditSend + deferredSend into a single pendingSend ref with a reason discriminator. Edit path now uses flushSync to commit state synchronously and calls sendMessage immediately — no timing gap, no race condition. Persona path keeps the deferred effect with the unified ref. Blocker 3: Re-add !isUser guard on retry button in MessageBubble so retry only shows on assistant messages (edit covers user messages). Bonus: TODO comment on rebuildAttachmentDrafts dedup heuristic explaining the assumption that all images produce both a content block and metadata entry.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3fc723c0c8
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| await sendMessage( | ||
| textContent.text, | ||
| text, | ||
| targetPersonaId | ||
| ? { id: targetPersonaId, name: targetPersonaName } | ||
| : undefined, |
There was a problem hiding this comment.
Route retry sends through persona/provider switching logic
This retry path calls sendMessage directly with only targetPersonaId, but sendMessage still selects the provider from providerOverride (the current session provider). If a user retries an older message after switching providers, the resend can go to the wrong backend even though the persona metadata is preserved. ChatView.handleSend already contains the provider-switch flow for persona changes, so bypassing it here can change model/provider behavior for retries.
Useful? React with 👍 / 👎.
| } else if (e.key === "Enter" && !e.shiftKey) { | ||
| e.preventDefault(); | ||
| handleEditSave(); |
There was a problem hiding this comment.
Ignore Enter during IME composition in inline edit
Pressing Enter in this handler always saves (unless Shift is held), but there is no composition guard (isComposing). For CJK/IME users, Enter is used to confirm a candidate, so this can unexpectedly submit the edit and truncate/re-send conversation history mid-composition. The Enter branch should be skipped while composition is active.
Useful? React with 👍 / 👎.
I think this PR doesn't do that exactly (based on some quick pairing with an LLM). The goose backend seems to delete the session history if you edit a message so the user sees the session as if it was rewound and that message was changed, but the LLM sees the full history. So if you edit a message the LLM thinks this is the history: I'm fine for us to merge this in as the behaviour, but let's make that clearer in the PR description. I'm not sure there is a better way at the moment without the proper ACP truncate+fork feature that seems to be coming soon. Also, it seems all user messages are editable, so the difference between what the user sees and what the LLM thinks the history is could be very different. Thinking about it a bit more: is it worth having the feature if it doesnt actually revise the LLM's history? Is there a benefit? |
Category: new-feature
User Impact: Users can now edit sent messages in-place and retry from any point in a conversation, with all attachments, images, and persona overrides preserved through the re-send.
Problem: The Goose 2 desktop app had no way to correct a sent message or retry a failed exchange. Users who made a typo or wanted to refine their prompt had to start a new conversation.
Solution: Adds client-side inline edit and per-message retry using truncate-and-resend. Edit mode renders a textarea over the original message bubble with Save/Cancel controls and keyboard shortcuts (Enter to save, Escape to cancel, Shift+Enter for newline). Retry identifies the originating user message, reconstructs its full context (text, images, file attachments, persona), and re-sends after truncation. A Radix HoverCard replaces the old CSS group-hover for the action toolbar, fixing z-index and animation issues. Session forking support can be layered on as a follow-up.
File changes
ui/goose2/src/features/chat/hooks/useChat.ts
Added
retryMessage,editMessage, andcancelEditto the chat hook.retryMessagevalidates content and reconstructs attachment drafts before truncating history, then forwards text, persona, and attachments tosendMessage.editMessageenters non-destructive edit mode; truncation only happens on save.ui/goose2/src/features/chat/hooks/tests/useChat.test.ts
Added 14 hook-level tests: 5 for
retryMessage(truncation, persona preservation, assistant-to-user lookup, streaming/thinking guards), 3 foreditMessage(state, streaming guard, assistant-role guard), 1 forcancelEdit, 3 for attachment preservation (image-only, file metadata, mixed content), and 2 forretryLastMessagedelegation.ui/goose2/src/features/chat/lib/attachments.ts
Added
rebuildAttachmentDrafts()— reconstructsChatAttachmentDraft[]from a stored message'sImageContentblocks andmetadata.attachments, enabling retry and edit to preserve the original message's full attachment context.ui/goose2/src/features/chat/stores/chatStore.ts
Added
editingMessageIdBySessionstate andsetEditingMessageIdaction. Editing state is scoped per-session and cleaned up on session cleanup.ui/goose2/src/features/chat/stores/tests/chatStore.test.ts
Added tests for
setEditingMessageIdset, clear, and session cleanup.ui/goose2/src/features/chat/ui/ChatView.tsx
Added
handleSaveEdit— stops streaming if active, reads persona and attachments from the original message before truncation, then defers the re-send via a ref until chat state returns to idle. WiresonRetryMessage,onEditMessage,onSaveEdit,onCancelEdit, andeditingMessageIdtoMessageTimeline.ui/goose2/src/features/chat/ui/ChatInput.tsx
Syncs textarea text state when
initialValueprop changes (needed for edit mode to populate the input). AddedinitialValuesync effect.ui/goose2/src/features/chat/ui/MessageBubble.tsx
Replaced CSS group-hover action toolbar with Radix HoverCard. Added inline edit UI: textarea overlay with Save/Cancel buttons, keyboard handling (Enter/Escape/Shift+Enter), empty-text guard. User bubbles get muted background and max-width constraint.
ui/goose2/src/features/chat/ui/MessageTimeline.tsx
Passes
onRetryMessage(assistant messages),onEditMessage(user messages),editingMessageId,onSaveEdit, andonCancelEditthrough toMessageBubble.ui/goose2/src/features/chat/ui/tests/ChatInput.test.tsx
Added test for
initialValuesync — verifies textarea updates when the prop changes.ui/goose2/src/features/chat/ui/tests/MessageBubble.test.tsx
Added 10 tests: inline edit keyboard shortcuts (Enter saves, Escape cancels, Shift+Enter allows newline), empty-text guard, edit/cancel button rendering, assistant edit-button exclusion, user edit-button presence, and HoverCard hover behavior.
ui/goose2/src/shared/i18n/locales/en/chat.json
Added
edit.hint,edit.textareaAriaLabelkeys for the inline edit UI.ui/goose2/src/shared/i18n/locales/en/common.json
Removed dead
messageBranchi18n keys (unused forking infrastructure).ui/goose2/src/shared/i18n/locales/es/chat.json
Added Spanish translations for
edit.hint,edit.textareaAriaLabel.ui/goose2/src/shared/i18n/locales/es/common.json
Removed dead
messageBranchi18n keys (unused forking infrastructure).ui/goose2/src/shared/ui/ai-elements/message.tsx
Removed
MessageBranchand 5 sub-components (~210 lines of dead code). Cleaned 11 unused imports.ui/goose2/src/shared/ui/hover-card.tsx
Added
barevariant to HoverCard — no background, border, or shadow — used by the message action toolbar.Reproduction Steps
Demo
Screen.Recording.2026-04-14.at.12.47.56.PM.mov