fix(conversation): drop duplicate margin on nested text bubbles - #390
Open
RonenMars wants to merge 100 commits into
Open
fix(conversation): drop duplicate margin on nested text bubbles#390RonenMars wants to merge 100 commits into
RonenMars wants to merge 100 commits into
Conversation
Add typed cache integrity alert contracts across REST, WebSocket, and per-server Zustand state. Sync pending alerts through websocket broadcasts and foreground-aware REST queries so clients converge after reconnects or missed unicasts. Present low-severity alerts in a banner and auto-open high-severity alerts in a localized resolution modal. Support prune-all, prune-selected, ignore, and reset-rescan actions; require confirmation for destructive choices; refetch on fingerprint conflicts; and surface backup paths on success. Cover store lifecycle, websocket dispatch, banner and modal interactions, locale parity, backward-compatible 404 handling, successful resolution, and fingerprint conflicts with unit and mocked-transport integration tests.
Mark Features 35/36 and Bugs 5/16 as in-flight; freeze the 2026-07-18 analysis as a snapshot with an updated release verdict. Co-authored-by: Cursor <cursoragent@cursor.com> [skip-ci]
Discard empty PTYs the user never typed into so they do not linger in the hub after abandoning a new session from browse. Co-authored-by: Cursor <cursoragent@cursor.com>
Add typed cache integrity alert contracts across REST, WebSocket, and per-server Zustand state. Sync pending alerts through websocket broadcasts and foreground-aware REST queries so clients converge after reconnects or missed unicasts. Present low-severity alerts in a banner and auto-open high-severity alerts in a localized resolution modal. Support prune-all, prune-selected, ignore, and reset-rescan actions; require confirmation for destructive choices; refetch on fingerprint conflicts; and surface backup paths on success. Cover store lifecycle, websocket dispatch, banner and modal interactions, locale parity, backward-compatible 404 handling, successful resolution, and fingerprint conflicts with unit and mocked-transport integration tests.
Explicit Help & Feedback submissions self-init Sentry like reportOneShot, show delivery method in the success UI, and keep standing reporting opt-in. Co-authored-by: Cursor <cursoragent@cursor.com>
Legacy uploads with spaces in filenames broke Claude Code path parsing; escape spaces so multi-attachment sends remain valid single-token refs. Co-authored-by: Cursor <cursoragent@cursor.com>
…orktree-merge-open-prs # Conflicts: # __tests__/unit/components/servers/CacheAlertModal.test.tsx # components/servers/CacheAlertModal.tsx # services/api-client.ts # services/query-client.ts # stores/servers.ts # types/api.ts
…orktree-merge-open-prs
…worktree-merge-open-prs
…to worktree-merge-open-prs
…19' into worktree-merge-open-prs [skip-ci] [skip-ci]
Adds missing viaSentry/viaEmail keys to feedback.json for ar, he, and ru. The English keys were added in this branch but the other locales were never updated, which i18n-completeness.test.ts catches.
Bumps the npm_and_yarn group with 1 update in the / directory: [shell-quote](https://github.com/ljharb/shell-quote). Updates `shell-quote` from 1.8.4 to 1.10.0 - [Changelog](https://github.com/ljharb/shell-quote/blob/main/CHANGELOG.md) - [Commits](ljharb/shell-quote@v1.8.4...v1.10.0) --- updated-dependencies: - dependency-name: shell-quote dependency-version: 1.10.0 dependency-type: indirect dependency-group: npm_and_yarn ... Signed-off-by: dependabot[bot] <support@github.com>
The conversation screen now polls its delta drain while focused and consumes streamed conversation_events, so an externally launched session reads as a live transcript without resuming it. Resuming a conversation that may still be open elsewhere shows a confirmation naming what the server detected, and offers Take over only when a real process was matched. External sessions route to the read-only conversation view instead of the PTY screen, and the discovered-session overtake path is gated off for them. Session rows show a distinct external-alive indicator derived from the additive liveness fields, falling back to pid for older servers.
Pass Sentry credentials through to the Ship iOS workflow step so the source-map upload in the Xcode bundle phase can authenticate.
Automated version bump after successful TestFlight upload (build 165). Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…search (#351) ## What Adds a "Rollup / Snapshot Tag Naming" section to `CLAUDE.md` documenting the `test-<env>/v<version>-<sha>-<date>` convention for throwaway pre-merge verification snapshots across the `dev` / `pre-release` / `production` environments. Updates the deploy workflow's `deploy_ref` input description and both non-main warning messages to state that a tag or commit SHA is accepted, not just a branch. Adds a research note under `docs/research/` comparing the common industry best practices for building one combined verification artifact from several open PRs, ranked by relevance to this repo. ## Why We routinely combine several open PRs into one build to verify them together before merging each individually, and the naming and deploy story for that snapshot was undocumented. The research note records the patterns behind the choice — immutable snapshot tag, build-once/promote-many with a release-candidate identifier, and Expo per-PR update channels — so the convention has a rationale on record. ## Notes `deploy_ref` already accepted tags via `actions/checkout`, so this is a wording clarification only with no change to workflow behavior. This branch bundles two commits: the research note and the convention change.
The conversation screen now polls its delta drain while focused and consumes streamed conversation_events, so an externally launched session reads as a live transcript without resuming it. Resuming a conversation that may still be open elsewhere shows a confirmation naming what the server detected, and offers Take over only when a real process was matched. External sessions route to the read-only conversation view instead of the PTY screen, and the discovered-session overtake path is gated off for them. Session rows show a distinct external-alive indicator derived from the additive liveness fields, falling back to pid for older servers.
Adds a dead-key test asserting every English key is referenced somewhere in the source, complementing the existing key-parity gate in i18n-completeness.test.ts.
The scanner reads `t('ns:key')` and bare `t('key')` calls, keeps the static prefix of dynamic keys as a prefix match, and picks up keys hoisted into consts before reaching t().
Arrays are treated as leaves since they are fetched whole via `returnObjects: true`.
Removes the 161 keys it flagged across all four locales — 654 removals, since ar and ru carry extra CLDR plural forms.
Every flagged key was verified against a raw grep to rule out false positives.
Repoints the plural and interpolation assertions in i18n.test.ts onto keys the app actually uses, since the previous fixtures were among the dead keys.
Both server remove confirmations built their message from a hardcoded English template literal, so the dialog body stayed in English for he, ru, and ar users while the surrounding title and buttons translated.
The servers:dialog.removeMessage key already existed but had gone unreferenced, which is how the hardcoding went unnoticed.
Points both call sites at the key and adds a {{server}} placeholder to it, since the previous wording was generic while the hardcoded string named the server.
The English output is unchanged; the other three locales are rephrased to match.
Mark in-flight and shipped items from the July launch batch, reconcile stale Open rows, and set the next mobile actions (typecheck, Maestro, onboarding). Co-authored-by: Cursor <cursoragent@cursor.com> [skip-ci]
Unblocks `voice_dictation` by granting speech-recognition and microphone via simctl privacy in ensure-release-build.js. Co-authored-by: Cursor <cursoragent@cursor.com>
Guard Skip/swipe so Welcome cannot dump into an empty Hub, advance immediately after QR exchange, add back-to-choose, and fix Mac-centric copy / default port 8766. Co-authored-by: Cursor <cursoragent@cursor.com>
Attribute browse failures to setup/XCUITest flake rather than FilterSortSheet, add layout/close testIDs, and make feat1 switch Classic→Tree before drilling. Co-authored-by: Cursor <cursoragent@cursor.com>
… [skip-ci] [skip-ci]
The API returns a JSONL-derived conversation name, but mobile discarded it: the list and detail adapters hardcoded title from project_name, and the live-session header never read the session's name field. Wire session_name through the Raw/Session types and both adapters, and add it to the live-view and session-row fallback chains (user rename -> session name -> project name).
…llow-up chain [skip-ci]
The API returns a JSONL-derived conversation name, but mobile discarded it: the list and detail adapters hardcoded title from project_name, and the live-session header never read the session's name field. Wire session_name through the Raw/Session types and both adapters, and add it to the live-view and session-row fallback chains (user rename -> session name -> project name).
…live pause toggle Keep the transcript on screen during a live reload instead of blanking. Every delta drain rebuilt ConversationDetail from raw pages, giving each Message a fresh object identity even when byte-identical, so FlashList (startRenderingFromBottom) dropped and re-measured every cell — a blank "blink". reuseMessageIdentities reuses the prior object for any unchanged id, so existing rows keep stable references and only genuine appends read as new. Fade newly-arrived tail messages in from the bottom with Reanimated FadeInDown, gated so only live appends animate — cold-open history and older-page backfill do not. Add a live indicator and pause/resume toggle to the read-only conversation view. Pausing stops the freshness poll and conversation_updated drains and freezes the rendered snapshot; resuming runs one catch-up drain and re-enables live updates. The control appears only while the session is actually live.
…live pause toggle Keep the transcript on screen during a live reload instead of blanking. Every delta drain rebuilt ConversationDetail from raw pages, giving each Message a fresh object identity even when byte-identical, so FlashList (startRenderingFromBottom) dropped and re-measured every cell — a blank "blink". reuseMessageIdentities reuses the prior object for any unchanged id, so existing rows keep stable references and only genuine appends read as new. Fade newly-arrived tail messages in from the bottom with Reanimated FadeInDown, gated so only live appends animate — cold-open history and older-page backfill do not. Add a live indicator and pause/resume toggle to the read-only conversation view. Pausing stops the freshness poll and conversation_updated drains and freezes the rendered snapshot; resuming runs one catch-up drain and re-enables live updates. The control appears only while the session is actually live.
…live pause toggle Keep the transcript on screen during a live reload instead of blanking. Every delta drain rebuilt ConversationDetail from raw pages, giving each Message a fresh object identity even when byte-identical, so FlashList (startRenderingFromBottom) dropped and re-measured every cell — a blank "blink". reuseMessageIdentities reuses the prior object for any unchanged id, so existing rows keep stable references and only genuine appends read as new. Fade newly-arrived tail messages in from the bottom with Reanimated FadeInDown, gated so only live appends animate — cold-open history and older-page backfill do not. Add a live indicator and pause/resume toggle to the read-only conversation view. Pausing stops the freshness poll and conversation_updated drains and freezes the rendered snapshot; resuming runs one catch-up drain and re-enables live updates. The control appears only while the session is actually live.
…ites The live-view session screen calls useNavigation() and reads stopSession.mutate in an unmount effect, but the SessionScreen integration suites mocked expo-router and useSessionActions without those members, so every render threw and the Integration tests CI job failed. Add useNavigation to all seven suites' expo-router mock and stopSession to the four useSessionActions mocks that lacked it, matching the shapes already used by the passing suites.
…live pause toggle
Reuse search-target + match navigation so users can search the open conversation without going through hub-wide list search. Co-authored-by: Cursor <cursoragent@cursor.com>
## Summary Live session screen messages could appear missing on arrival until a tiny (5-15px) manual scroll revealed them. Root cause: `LiveConversationView` drove scroll with JS `scrollToEnd()` effects that raced FlashList's cell-layout pass for the newly appended row, landing short. Fix: adopt FlashList's native `maintainVisibleContentPosition` bottom-anchoring (same config already used by `ConversationHistoryList`) and drop the racing scroll effects. ## Test plan - [x] `npx tsc --noEmit` — no new errors - [x] `npx eslint components/conversation/LiveConversationView.tsx` — clean - [ ] Manual verification on device/simulator: send messages in a live session, confirm no scroll-to-reveal needed
## Summary Messages with a tool/diff/thinking block plus a text block showed a visibly larger gap to adjacent rows than plain text-to-text messages. Root cause: `MessageBubble`'s own `marginVertical` stacked with `MessageItem`'s `toolContainer` margin when the bubble was nested inside it (the tool/diff + text branch), doubling the effective spacing there. Fix: added a `noOuterMargin` prop to `MessageBubble`, passed from the nested usage in `MessageItem`, so spacing in that branch comes only from `toolContainer`. ## Test plan - [x] `npx tsc --noEmit` — no new errors - [x] `npx eslint components/conversation/MessageBubble.tsx components/conversation/MessageItem.tsx` — clean - [ ] Manual verification: view a conversation with a tool-call + prose message adjacent to plain text messages, confirm consistent spacing
A session parked on a question/permission card replays only blank ring-buffer rows over the WS terminal_replay. The handler treated any replay as a successful load, latching replayReceivedRef and clearing the 2s HTTP fallback timer, so the terminal stayed blank forever even though GET /api/sessions/:id/output still held the full transcript. Only accept a replay that carries at least one non-blank line; otherwise leave the fallback timer armed so /output fills the screen.
Sessions open via router.push and the native stack keeps pushed-under screens mounted, so each opened session left its WS handlers firing on every frame and its VirtualTerminal buffer growing. The app slowed down linearly with the number of sessions opened in one run (noticeable after 5-6). freezeOnBlur suspends a hidden screen's render and effects until it's focused again.
…der cost An append-only session grew the VirtualTerminal grid without limit; only the rendered copy was capped (slice(-maxLines)), so getLines() stayed an O(total-lines) scan on every terminal_output frame. Combined with retained background session screens this compounded the cumulative slowdown. Trim the oldest rows past a 10k cap and shift the cursor to match, so memory and per-frame cost stay bounded regardless of transcript length.
…o integration-merge-354-355-376 # Conflicts: # app/conversation/[id].tsx
…gration' into integration-merge-354-355-376 # Conflicts: # app/conversation/[id].tsx
…ntegration-merge-354-355-376
Claude Code repaints its spinner line only when it has other output to draw, so the elapsed counter in "✽ Swirling… (56s · ↑ 3.4k tokens)" sticks at whatever it was on the last paint.
On mobile a number that stops moving reads as a hung session, which is what users reported.
Verified against a raw node-pty probe: during ~50s of silent thinking the counter repainted once, and even while streaming it jumped 3s → 25s.
The existing chrome filters already dropped the bare "✽ Swirling…" spinner but not the variant carrying the counter, so that one line survived to the render path.
Filter it, along with the completed-turn timers ("✻ Crunched for 3m 17s"), and require the parenthetical to start with an elapsed-seconds value so ordinary output like "Testing… (this is real output)" is preserved.
Also widen the model status-line filter: it required a minor version and an ASCII pipe, so "Sonnet 5 │ ~/path" leaked through.
Surface model, effort and permission mode in the Session Info sheet from the new GET /api/sessions/:id fields.
InfoModal skips undefined values, so these rows simply do not render against a streamer that does not send them.
RonenMars
force-pushed
the
fix/message-bubble-double-margin-main
branch
from
July 23, 2026 21:23
153814c to
46058fd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Cherry-pick of the fix already merged into
integration-dev/v1.0.0-2026-07-22via #384, ontomain.Summary
Messages with a tool/diff/thinking block plus a text block showed a visibly larger gap to adjacent rows than plain text-to-text messages.
Root cause:
MessageBubble's ownmarginVerticalstacked withMessageItem'stoolContainermargin when the bubble was nested inside it (the tool/diff + text branch), doubling the effective spacing there.Fix: added a
noOuterMarginprop toMessageBubble, passed from the nested usage inMessageItem, so spacing in that branch comes only fromtoolContainer.Test plan
npx tsc --noEmit— no new errorsnpx eslint components/conversation/MessageBubble.tsx components/conversation/MessageItem.tsx— clean