Skip to content

fix(app): stop the composer showing a mic while it holds text - #1673

Open
chphch wants to merge 1 commit into
slopus:mainfrom
chphch:fix/send-icon-lags-typing
Open

fix(app): stop the composer showing a mic while it holds text#1673
chphch wants to merge 1 commit into
slopus:mainfrom
chphch:fix/send-icon-lags-typing

Conversation

@chphch

@chphch chphch commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

The mobile composer folds send, stop and the mic into one button, and which one it draws comes from hasText — state the keystroke handler sets inside React.startTransition. React may defer that update for as long as higher-priority work keeps arriving, so the composer can hold a typed message while the button still shows a mic. A session streaming a reply is exactly that kind of work, which is why it surfaces mid-conversation and is hard to attribute to any one action; I only found it because a user reported "the send button shows up as the voice button" and could not say when.

What makes it worth fixing rather than tolerating is that the button's behaviour is already correct — handleMobilePrimaryPress and handleSendPress both read the live text and send. The comment above the first one says as much. So the stale icon is not a brief cosmetic lag; it is the button advertising dictation while it would in fact send. The user confirmed that: tapping the mic-looking button sent the message.

hasText only ever changes at the empty/non-empty boundary, so the handler now compares against a ref and calls setState only on the flip. Ordinary keystrokes hand React nothing at all — strictly less work than the transition they used to schedule, which was the reason the transition was there — and the one update that matters is urgent.

Every path that changes the text funnels through onChangeText, including the imperative setTextAndSelection on both the native and web MultiTextInput (autocomplete insert, Escape, and SessionView's post-send clear), so the ref cannot drift out of step with the field.

The send button and the mic are one control, and which one it draws comes
from `hasText` — state the keystroke handler updates inside
`React.startTransition`. React is free to defer that, so the composer can
hold a typed message while the button still shows a mic, for as long as
higher-priority work keeps arriving. A session streaming a reply is
exactly that, which is why this shows up mid-conversation and is hard to
pin to any one action.

The button's behaviour is already right — both press handlers read the
live text and send. That is what makes the stale icon worth fixing rather
than tolerating: it is not a brief cosmetic lag, it is the button
advertising dictation while it would in fact send.

`hasText` only changes at the empty/non-empty boundary, so compare against
a ref and call setState only when it actually flips. Ordinary keystrokes
now hand React nothing at all — strictly less work than the transition
they used to schedule — and the one update that matters is urgent.

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
@chphch
chphch force-pushed the fix/send-icon-lags-typing branch from 3874602 to daf980c Compare August 22, 2026 00:21
@chphch

chphch commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

@bra1nDump — after the August 22 batch, here are six more of the same shape, in one comment rather than six pings.

Each one repairs a regression I traced to a specific commit, each is still reproducible on 3cf9ce70f (I re-checked every line today), and all six merge clean as they stand — no rebase needed.

PR Broken by What it does on current main Size
#1610 1677fadc FAB's + glyph is invisible — the commit made the native fill transparent under MobileGlassSurface but kept color={theme.colors.fab.icon} (FAB.tsx:69,72), so it paints near-white on near-white in light mode and near-black on near-black in dark. Same class as #1582 / #1598 / #1599; this is the one control that sweep missed. 1 file, +10/-1
#1673 514ef3f1 + 1072d782 Composer shows a mic while it holds text. 514ef3f1 put hasText behind React.startTransition (AgentInput.tsx:897); 1072d782 then folded the mic into the mobile primary button. While an agent streams, the transition is starved, so the button draws a mic — sending still works, because the press handler reads live text. 1 file, +17/-6
#1378 514ef3f1 Sibling of the above at AgentInput.tsx:904: setInputState is the only input to the slash/@ autocomplete, so under the same starvation the menu silently stops appearing mid-session. 1 file, +8/-3
#1382 53850917 Complex Mermaid diagrams render as an oversized empty box. The commit added reportHeight() but measures document.body.scrollHeight (MermaidRenderer.tsx:146-149), so a wide diagram reports the page height. Closes #1475 (kemier, open and uncommented since Jul 3). 1 file, +33/-20
#1451 98c6ad9e + 971d6089 A selection card folds out of view, so the user cannot answer a prompt they cannot see. 98c6ad9e replaced the "text always passes through as a standalone TextItem" path with collectAgentWorkGroups; 971d6089 made collapse the default. useGroupedMessages.ts has no selection-awareness today. Same surface as #1592. 2 files, +257/-21
#1636 53850917 + b042d834 Aborting a turn silently re-models a running session: 53850917 clears permissionMode/modelMode/effortLevel on abort (SessionView.tsx:842), and b042d834 made resolveMessageModeMeta fall back live to agentDefaultOverrides (messageMeta.ts:54,63). Merging #1606 as 517e7492 removed the mask that made this hard to reproduce on main. 4 files, +143/-5

Two things I'd rather say up front than have you find:

No rush on any of these; I know the queue is long. Happy to split, shrink, or close any of them if a different shape suits you better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant