fix(web): retain composer text on send failure (closes #776)#773
fix(web): retain composer text on send failure (closes #776)#773heavygee wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Findings
- [Major] Preserve scheduled send state on failed sends — the new error handoff only carries
textanderror, butSessionChatclearspendingScheduleas soon as the mutation is accepted. If a scheduled send then fails with 4xx/5xx/network, the composer is restored as an immediate send, so editing and pressing send changes the delivery semantics. Evidence:web/src/hooks/mutations/useSendMessage.ts:35.
Suggested fix:Then carryexport type SendErrorInfo = { text: string error: unknown scheduledAt: number | null } options?.onError?.({ text: input.text, error, scheduledAt: input.scheduledAt ?? null, })
scheduledAtthroughComposerSendErroras a future{ type: 'absolute', ms: scheduledAt }and restore it via the existingonSchedulepath when applying the send error.
Summary
Review mode: initial. One issue found in the restored-composer state for scheduled failed sends.
Testing
Not run (automation: bun is not installed in this runner). git diff --check passed.
HAPI Bot
|
Addressed the Major finding in 2b48cf4.
Also corrected the issue ref - this PR closes upstream #776 (the original "closes #15" referenced an unrelated, already-closed upstream issue; my mistake). |
There was a problem hiding this comment.
Findings
- [Major] Remove the failed optimistic row when restoring the composer —
onErrornow restores the same text into the composer, but the mutation still leaves the optimistic message in the visible thread asfailed. Failed messages are intentionally not filtered out of the thread, so the user sees the failed bubble plus the restored composer text; editing and sending from the new affordance creates a duplicate stale failed turn. Evidence:web/src/hooks/mutations/useSendMessage.ts:148.
Suggested fix:import { appendOptimisticMessage, getMessageWindowState, removeOptimisticMessage, updateMessageStatus, } from '@/lib/message-window-store' onError: (error, input) => { removeOptimisticMessage(input.sessionId, input.localId) haptic.notification('error') options?.onError?.({ text: input.text, error, scheduledAt: input.scheduledAt ?? null }) }
Questions
- None.
Summary
- Review mode: follow-up after new commits. The previous scheduled-send finding is addressed, but the new composer-restore path now duplicates failed sends in the visible thread unless the failed optimistic row is removed or otherwise hidden.
Testing
- Not run (automation:
bunis not installed in this runner). The added hook tests do not cover the composer/thread integration where the duplicate failed bubble appears.
HAPI Bot
|
Addressed Major #2 in 10dcc32.
Test in Full web suite green: 702/702. |
There was a problem hiding this comment.
Findings
-
[Major] Key restored send errors by the target session —
onErrornow restores the rejected text into whicheverSessionPageis mounted, but the error payload does not include the session id actually sent. In the inactive-session resume path,useSendMessagecan resolvetargetSessionId, start async navigation, and then the POST can fail before navigation completes; the old session composer gets restored and thesessionIdchange effect clears it, so the typed text is lost again. Evidence:web/src/router.tsx:333.
Suggested fix:export type SendErrorInfo = { sessionId: string text: string error: unknown scheduledAt: number | null } options?.onError?.({ sessionId: input.sessionId, text: input.text, error, scheduledAt: input.scheduledAt ?? null }) const [sendErrors, setSendErrors] = useState<Record<string, ComposerSendError>>({}) const clearSendError = useCallback(() => { setSendErrors((prev) => { const next = { ...prev } delete next[sessionId] return next }) }, [sessionId]) onError: (info) => { sendErrorIdRef.current += 1 setSendErrors((prev) => ({ ...prev, [info.sessionId]: { id: sendErrorIdRef.current, text: info.text, message: deriveSendErrorMessage(info.error, t), scheduledAt: info.scheduledAt } })) }
-
[Major] Do not delete failed attachment sends unless attachments are also restored — the new failure path always removes the optimistic row, but
SendErrorInfocarries onlytextandscheduledAt. For a rejected attachment or attachment-only send, assistant-ui has already cleared the composer, the failed row is removed atremoveOptimisticMessage, and no code re-adds the uploaded attachment metadata; the operator loses both the attachment preview and a retry surface. Evidence:web/src/hooks/mutations/useSendMessage.ts:149.
Suggested fix:function getMessageAttachments(message: DecryptedMessage): AttachmentMetadata[] | undefined { const content = message.content if (content.role !== 'user' || content.content.type !== 'text') return undefined return content.content.attachments } onError: (error, input) => { if (input.attachments?.length) { updateMessageStatus(input.sessionId, input.localId, 'failed') haptic.notification('error') return } removeOptimisticMessage(input.sessionId, input.localId) haptic.notification('error') options?.onError?.({ text: input.text, error, scheduledAt: input.scheduledAt ?? null }) } mutation.mutate({ sessionId, text: message.originalText, localId, createdAt: message.createdAt, attachments: getMessageAttachments(message), scheduledAt: message.scheduledAt ?? null, })
Questions
- None.
Summary
- Review mode: follow-up after new commits. The prior failed-row duplicate finding is addressed for text-only sends, but the full diff still has failure-restore gaps for resumed sessions and attachment sends.
Testing
- Not run (automation:
bunis not installed in this runner). Suggested coverage: inactive-session resume where the resumed-session POST fails before route navigation, plus rejected text+attachment and attachment-only sends.
HAPI Bot
|
Addressed Major #3 + Major #4 in 29cc498. Major #3 (sessionId carry-through): Major #4 (attachment sends): Forked
Also fixed a latent bug the bot's snippet flagged: Three new tests cover the cases:
Full web suite: 705/705. |
There was a problem hiding this comment.
Findings
- No issues found.
Questions
- None.
Summary
- Review mode: follow-up after new commits. The latest head addresses the previous bot findings for resumed-session failures and attachment-send failures. Residual risk: this review did not execute the web tests; I validated from the diff and surrounding source only.
Testing
- Not run (automation/security review; did not execute PR code). Existing added coverage includes text-only 4xx/5xx/network failure retention, scheduled failure retention, resumed-session failure targeting, attachment failure fallback, and attachment retry preservation.
HAPI Bot
When the message composer submits and the hub responds with a 4xx/5xx
or the fetch fails outright, assistant-ui clears the composer
synchronously the moment send is invoked. Without intervention the
operator's typed text is destroyed at exactly the moment they most
need it preserved. SessionChat additionally clears any pending
schedule on accept, so a failed scheduled send was also silently
downgrading to immediate on the next attempt.
Behaviour:
- useSendMessage exposes onError({ sessionId, text, scheduledAt, error })
so the route can hand the input back to the composer. sessionId is
the resolved target (post-resolveSessionId), so an inactive-session
resume that resolves a new id, kicks off async navigation, then
fails the POST restores into the resumed session's composer rather
than the old one.
- router.tsx stores sendErrors keyed by sessionId. Per-session lookup
replaces the clear-on-session-change effect, so errors do not bleed
between sessions and a session-scoped failure persists across
navigation.
- HappyComposer accepts ComposerSendError, restores text via
api.composer().setText() once per failure id, and re-establishes any
pending schedule via onSchedule({ type: 'absolute', ms: scheduledAt }).
It renders a red ring on the composer wrapper and a role="alert"
inline message; both clear the moment the operator types or sends.
- onError forks on input.attachments. Text-only sends use the
composer-restore path (removeOptimisticMessage drops the row so the
failed bubble does not duplicate the restored text). Attachment
sends keep the legacy failed-bubble UX (status='failed' + in-thread
retry button) because the composer-restore path can't reinstate
uploaded attachment metadata. retryMessage extracts attachments
from the stored optimistic message via getMessageAttachments so
failed-bubble retry of an attachment send re-fires with its files.
Acceptance (issue tiann#776):
- Submit -> 500/502/503/network error -> composer text not cleared
- Submit -> 400/401/403 -> composer text not cleared, error inline
- Submit -> 2xx -> composer clears as today
- Operator can edit retained text and retry without re-typing
- Failed scheduled sends restore as scheduled, not as immediate
Tests in web/src/hooks/mutations/useSendMessage.test.tsx cover
text-only 4xx/5xx/network retention, scheduled-send carry-through,
optimistic-row removal on text-only failure, sessionId carry-through
under resolveSessionId, attachment failure fallback, and attachment
retry preservation. Full web suite passes (705 tests). bun typecheck
clean. No SCHEMA_VERSION bump (frontend-only).
29cc498 to
bc6c338
Compare
There was a problem hiding this comment.
Findings
- No issues found.
Questions
- None.
Summary
- Review mode: follow-up after new commits. I reviewed the latest seven-file web diff in full and used the previous bot review plus compare diff only as follow-up context. No correctness, security, regression, data-loss, performance, or maintainability issues above the reporting threshold were found. Residual risk: composer UI behavior itself was validated from source and hook-level tests only; I could not run browser/UI automation in this runner.
Testing
git diff --checkpassed.- Not run:
bun --filter web run test web/src/hooks/mutations/useSendMessage.test.tsxbecausebunis not installed on PATH in this runner.
HAPI Bot
Closes #776.
Problem
Submitting a message while the hub is unreachable (a daily-rebuild restart blip, a 5xx, or any 4xx) clears the composer immediately, destroying the typed text. Operators have to retype before retrying.
@assistant-ui/react's composer clearscomposer.textsynchronously when send is invoked, so by the timeuseSendMessage's mutation rejects, the text is already gone. SessionChat additionally clears the pending schedule the moment the mutation is accepted, so a failed scheduled send was also silently downgrading to immediate on the next attempt.Fix
useSendMessageexposes a newonErroroption that hands the original input back to the caller alongside the thrown error AND the resolvedscheduledAt(absolute epoch-ms or null).router.tsxowns asendErrorstate, set fromonErrorand cleared on success or session change.HappyComposeracceptssendErrorand:api.composer().setText(sendError.text)once per failure id, restoring the typed text.scheduledAtis non-null, also callsonSchedule({ type: 'absolute', ms: scheduledAt })so the clock button re-activates and the schedule fires at the originally intended instant.role="alert"inline message; both clear the moment the operator starts editing.Acceptance (issue #776)
Tests
web/src/hooks/mutations/useSendMessage.test.tsx- seven new cases under "issue #776":bun typecheckand the fullbun --filter web run testsuite (701 tests) both pass on this branch.Notes
docs/operator/ordocs/plans/changes.