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".
| const cancelEdit = useCallback(() => { | ||
| store.setEditingMessageId(sessionId, null); | ||
| store.clearDraft(sessionId); | ||
| }, [sessionId, store]); |
There was a problem hiding this comment.
Keep unsent draft when cancelling inline edit
cancelEdit() now calls clearDraft(sessionId), which wipes the compose draft for the whole session, not just edit mode. Because ChatInput is driven from draftsBySession, a user who has partially typed a new message and then presses Cancel/Escape in inline edit will lose that unsent draft text unexpectedly.
Useful? React with 👍 / 👎.
| } else if (att.type === "file" && att.path) { | ||
| drafts.push({ | ||
| id: crypto.randomUUID(), | ||
| kind: "file", | ||
| name: att.name, |
There was a problem hiding this comment.
Skip image metadata when rebuilding retry attachments
This branch rebuilds every metadata.attachments file with a path as kind: "file", even when the same image was already reconstructed from content image blocks above. For path-based image uploads, retry/edit now resends both an image and an extra file attachment, which changes the prompt context (adds a file-path reference) compared with the original send.
Useful? React with 👍 / 👎.
…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".
| name: att.name, | ||
| path: att.path, | ||
| }); | ||
| } else if (att.type === "file" && att.path) { |
There was a problem hiding this comment.
Rehydrate pathless file attachments on retry/edit
rebuildAttachmentDrafts() currently only rebuilds file attachments when att.path is present, so any message metadata entry like { type: "file", name, mimeType } is dropped during retry/edit. That pathless shape is produced by browser-file attachments (see useChatInputAttachments.addBrowserFiles), so users who resend edited/retried messages lose those attachments and send a different prompt context than the original message.
Useful? React with 👍 / 👎.
| }); | ||
| } else if (att.type === "file" && att.path) { | ||
| // Skip image file entries when content blocks already provide the base64 | ||
| if (hasImageDrafts && att.mimeType?.startsWith("image/")) continue; |
There was a problem hiding this comment.
Deduplicate image attachments per file, not per message
The global hasImageDrafts check drops all image metadata attachments whenever any base64 image block exists, which can remove legitimate image file attachments that were not reconstructed as image drafts (for example, image paths that fell back to kind: "file" in addPathAttachments). In mixed cases, retry/edit silently omits those files, so the resent message no longer matches the original attachment set.
Useful? React with 👍 / 👎.
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