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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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
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.
- 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).