fix(mobile-chat): restore composer draft on genuine send error (mc#2908 F7) #3034

Merged
devops-engineer merged 1 commits from fix/mobile-chat-f7-composer-state-on-error into main 2026-06-19 06:03:51 +00:00
2 changed files with 38 additions and 0 deletions
@@ -219,6 +219,9 @@ export function MobileChat({
const composerRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [pendingFiles, setPendingFiles] = useState<File[]>([]);
// Preserve composer state so a genuine send error can restore the draft
// and attachments instead of silently dropping them (mc#2908 F7).
const lastComposerRef = useRef<{ text: string; files: File[] } | null>(null);
const {
messages,
@@ -254,6 +257,17 @@ export function MobileChat({
},
});
// Restore draft + attachments when a genuine send error surfaces, so the
// user can retry without retyping (mc#2908 F7). Clear the snapshot after
// restoring to avoid replaying on unrelated error changes.
useEffect(() => {
if (sendError && lastComposerRef.current) {
setDraft(lastComposerRef.current.text);
setPendingFiles(lastComposerRef.current.files);
lastComposerRef.current = null;
}
}, [sendError, setDraft, setPendingFiles]);
// The agent is "thinking" when the user's send is in flight OR the workspace
// reports an in-flight task — either way it's reachable, so clear any stale
// "unreachable" banner the moment it's working (core#2745). Mirrors ChatTab.
@@ -364,6 +378,9 @@ export function MobileChat({
// shipped this; mobile reused the hook but still blocked here.)
if ((!text && pendingFiles.length === 0) || !reachable) return;
clearError();
// Snapshot the composer so a subsequent genuine send error can restore
// the draft and attachments instead of leaving the input empty (mc#2908 F7).
lastComposerRef.current = { text, files: [...pendingFiles] };
setDraft("");
const files = pendingFiles;
setPendingFiles([]);
@@ -762,6 +762,27 @@ describe("MobileChat — multi-send tap path (CR2 #2762)", () => {
expect(container.textContent ?? "").not.toContain("unreachable");
});
});
it("restores composer draft after a genuine send error (mc#2908 F7)", async () => {
vi.spyOn(api, "post").mockRejectedValueOnce(new Error("boom"));
const { container } = renderChat(mockAgentId);
const ta = container.querySelector("textarea") as HTMLTextAreaElement;
const sendBtn = container.querySelector('[aria-label="Send"]') as HTMLButtonElement;
await act(async () => {
fireEvent.change(ta, { target: { value: "retry me" } });
});
await act(async () => {
sendBtn.click();
});
await waitFor(() => {
expect(container.textContent ?? "").toContain("unreachable");
});
// Composer should be restored so the user can retry without retyping.
expect(ta.value).toBe("retry me");
});
});
describe("MobileChat — tool-call chain (#231 desktop parity)", () => {