fix(mobile-ui): batch 1 — 6 mobile UI fixes + 5 follow-ups (#21-#26)#29
Merged
Conversation
Long unbroken strings (Cloudflare R2 object URLs, thread ids) were rendered on a single line and bled past the card edge on mobile, forcing horizontal page scroll. Add wrap-break-word to the primitive value div and the thread-id meta line so URLs break inside their container.
…all (#22) Two changes: 1. Stat tiles: bump the multi-column breakpoint from sm (640px) to md (768px) so 8 tiles get 2 cols on phones + tablet portrait, 3 cols on small landscape, 4 cols on desktop — prevents the squished 2-col layout from looking cramped. 2. Waterfall timeline: change the outer container from overflow-hidden to overflow-x-auto/overflow-y-hidden so long bars can be reached by horizontal scroll on narrow viewports. Sticky label column + header remain pinned to the left edge during scroll.
The mobile sheet sidebar was passing hideSettings to UserButton, dropping the Settings entry from the expanded menu on mobile while desktop kept it. Drop the prop so both surfaces render the same menu items via the shared UserButton source — single source of truth, no per-renderer drift.
autoFocus on the Composer caused the soft keyboard to pop up the moment the chat page loaded on mobile, eating half the viewport before the user tapped anything. Gate autoFocus on (pointer: fine) + maxTouchPoints===0 so the keyboard stays hidden until the user explicitly taps the composer. Edit-composer still autoFocuses (line 572) — it's edit-in-place, focus is the intended behavior.
auto-rows-fr + the big/wide card min-heights forced every bento cell to match the tallest sibling, leaving ~50% empty whitespace below each card on mobile. Switch auto-rows-fr to md+ only (single-column stack on phones sizes by content) and demote the min-heights to md+ so the wide / big cards keep their footers on tablet and desktop where the row needs to fill the row's tallest sibling.
…edge (#26) Two compounding layout issues made the destructive dialog feel hostile on mobile: 1. items-stretch is flex default — on a flex-col footer the buttons stretched to fill the modal width, sitting at the viewport edge. Pin cross-axis to items-start on mobile (sm:items-center for desktop row layout) so buttons take intrinsic width. 2. Modal itself was max-w-[calc(100%-2rem)] — only 16px gutter on each side, which leaves destructive actions tight against the edge. Bump to max-w-[min(calc(100%-3rem),28rem)] on mobile so the modal sits in the comfortable center; desktop sm:max-w-lg unchanged.
…mobile (#21) Original #21 fix wrapped the thread-id text but left the Expand + Delete buttons jammed into a 4-col grid alongside the icon + title, so on narrow viewports the Delete button sat tight against the card edge. On mobile: collapse the outer grid to 2 cols (icon + title) and wrap the two action buttons in a col-span-full flex-col container so each button takes the full row width. On md+: the wrapper becomes display: contents and the buttons slot back into the original 4-col grid for the desktop row layout.
… mobile (#22 follow-up) Three mobile-only observability follow-ups from the user's review pass: 1. Top axis header (the time-axis bar pinned to the top of the waterfall) was `position: sticky` on all viewports — on mobile the sticky bar stole a quarter of the sheet's already-tight vertical space and fought with the ScrollText-using thread row that scrolls behind it. Switch to `md:sticky md:top-0 md:z-20` so the sticky behavior only kicks in at md+ where the panel has room for it. 2. Waterfall columns were sized 240px label + remainder bar in math (LABEL_WIDTH constant) and pinned via inline `style={{ width }}` — on iPhone-class viewports 240/390 = 62 % is over half the screen and the right-side bar squeezed to < 40 % with no clipping safety. Cap every column div at `max-w-[50%] md:max-w-none` with `overflow-hidden` on mobile so neither side bleeds past its slice and text labels truncate cleanly. Desktop widths unchanged. 3. The hover-only TooltipPopup sat at `z-50 fixed`; on touch devices long-press events can fire it even though there's no cursor. Hide it via `hidden md:block` so it never renders below md.
…w-up) Original #25 fix kept a single mid-size grid step in the landing bento: below md (768) the cards stack in a single column, between md and lg (768-1024) they go back into a 2-col layout with auto-rows-fr + 260 / 140 px min-heights, and only at lg+ does the 4-col bento kick in. The md branch is the worst of both worlds — the smaller Dual-graph and Observability cards stretch their row-height up to the 260 px Chat anything card, leaving 50 % empty whitespace beneath their text. Same content, bigger card. Drop the md breakpoint entirely. Below lg everything is single- column flex (mobile layout); the asymmetric bento only appears at lg+ where there's room for the 2×2 / 2×1 / 1×1 spans to actually fit.
…mmarize actions (#21 v3) User feedback on the screenshots pointed out two stacked mobile issues with the memory view. Both are scoped to mobile — desktop layout is unchanged. 1. About-you rows. The Delete button sat inline-right of the value column, so a long value (or a JSON NestedValue block) pushed Delete half off the card edge. Switch the row grid to `grid-cols-[auto_1fr] md:grid-cols-[auto_1fr_auto]` and put `col-span-2` on the Delete button on mobile so it spans both columns and slots under the value. Reverts to a single grid column on md+ where inline-right is fine. 2. Thread summaries. Two compounding issues: - The header packed Collapse + Delete side-by-side, so on narrow viewports the visible order was Collapse, Delete, Summary content — Delete landed BETWEEN the controls and the data it acts on. Pull Delete OUT of the header into a new row below the CollapsibleContent so the order is now Collapse → Summary → Delete. Delete is always visible (collapsed or not), separated by a `border-t` so it reads as a distinct action footer rather than a third button in the controls. - The CollapsibleContent had `ps-[calc(...)] === 56px` left padding to indent the Q&A under the title. On a 390 px viewport that eats ~14% of the row width for an indent that doesn't visually connect to anything (the row is now single-column on mobile). Drop the calc padding on both the content and the loading skeleton — desktop reads fine without it.
…+Delete grouped, dialog footer right-aligned + tests Closes the round-3 / round-4 review feedback on the mobile thread and dialog layouts. #21 (round 4 — user edit): - About-you store Delete button is now `w-full` + `col-span-2` on mobile so it sits full-width below the value (not inline-right where long values / JSON NestedValue blocks pushed it off the card edge). Reverts to inline col 3 + auto width on md+. - Thread-summarize action row collapses/expands the gap between Expand and Delete and sandwiches the summary content between them on mobile (so the buttons stay visually adjacent as one action group), then on md+ the summary content swaps out for a second row below the header. Done by extracting SummaryContent as a React element variable and rendering it twice — once inside the mobile action wrapper, once below the desktop header row — with `md:hidden` / `hidden md:block` picking the visible copy at each breakpoint. (Both share the parent `<Collapsible open>` state, so the toggle stays coordinated despite the render split.) - Chevron rotation switched from `rotate-180` to `rotate-90` so it points up when the row is expanded (down chevron above content reads better than left-pointing under the title). - Dropped the `ps-[calc(...)]` left padding that pushed summary text 56 px to the right of the row origin on small viewports. #26 (round 2 — user edit + dialog refactor): - Dialog footer buttons (Cancel + Delete in both profile and thread confirmation dialogs) now have `w-full md:w-auto` so they fill the row on mobile and shrink to intrinsic width on desktop. Improves destructive-action reach on touch. - Dialog footer items-start → items-end on mobile so buttons right-align instead of sitting against the modal edge. Tests: - 6 new regression tests pinning the responsive className choices for the issues above. Pattern: render the row, find the element by role/data-slot, assert the responsive Tailwind class is present (and that the wrong-class guard is absent). - Existing tests updated to tolerate the new `SummaryContent` twice-render layout: the same Q&A text now appears in two DOM slots (mobile + desktop), so `getByText` assertions switched to `queryAllByText(...).length >= 1`. The toggle still drives both copies because they share the parent's `<Collapsible open>` state.
Five small adjustments from the user's mobile review pass on the
observability panel.
1. StatCard label clipping. The label sits in a flex row with the
icon, and on a 4-col grid of `5.5rem` cards the label string
(e.g. "Output") can be pushed too far right and run into the
next card. Add `line-clamp-1` to the icon row and
`overflow-hidden whitespace-nowrap text-ellipsis` to the label
so long labels truncate cleanly with "…" instead of bleeding
into the next card.
2. WaterfallRow sticky-left label. Add `bg-background` so the
sticky label column paints a solid background when the bar
column scrolls under it — without bg the icon + name were
bleeding through the column's right border as the right column
moved. Inline-aligned with the prior `sticky left-0` rule.
3. Legend strip background. Add `sticky left-0` to the legend
wrapper under the waterfall so the type chips stay pinned to
the left viewport edge during horizontal scroll (matches the
label column behavior). The container was `relative` already
via the parent, so `left-0` pins in place.
4. TooltipPopup + SpanDetails whitespace. Two cosmetic indentation
fixes that fell out of a fresh `pnpm lint` pass — no behavioral
change.
5. Stat tile grid + token labels.
- Force `grid-cols-4` at every breakpoint (was
`grid-cols-2 sm:grid-cols-3 md:grid-cols-4`). The previous
2-col mobile step is undesirable after the StatCard changes
above — every tile can stay in a 4-col layout down to phone
width now that the label truncates inside each card.
- "token" → "tok" on Input/Output/Total labels. The full
word was wrapping to two lines on phones and looking worse
than the abbreviation.
This was referenced Jul 10, 2026
Closed
Closed
There was a problem hiding this comment.
All reported issues were addressed across 7 files
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
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.
Summary
Mobile UI smoke pass — six narrowly-scoped fixes for issues you only notice on a phone, plus a chain of narrower follow-ups caught in review.
wrap-break-wordon memory profile value cells so R2 object URLs and thread ids wrap inside the card. Follow-up: stack the threads-summarize Expand + Delete buttons vertically on mobile so they don't sit jammed against the card edge; round 4 refactor (with help from the test author) extractsSummaryContentas a React element rendered twice (md:hiddenmobile copy inside the action wrapper,hidden md:blockdesktop copy below the header) so the two buttons read as one action group on mobile while keeping the desktop 4-col grid unchanged.smtomd; flip timeline container fromoverflow-hiddentooverflow-x-auto / overflow-y-hidden; follow-ups: drop the top headerstickyon mobile (too tall), cap waterfall columns atmax-w-[50%]withoverflow-hiddenon mobile (240 px label was 62 % of an iPhone viewport), hide the hoverTooltipPopupbelowmd. v5 polish: per-card label clip (line-clamp-1+text-ellipsis), forcegrid-cols-4at every breakpoint, shortentoken→tokon the tile values, sticky-labelbg-background, legend stripsticky left-0.hideSettingsprop from the mobile sheet's<UserButton/>so the built-in Settings entry renders the same on mobile and desktop.autoFocuson(pointer: fine) && maxTouchPoints === 0so the soft keyboard stays hidden until the user taps the composer on touch devices. Edit-composer still autoFocuses (line 572).auto-rows-frand the big/widemin-h-*from the default (mobile) layer; follow-up: kill themd:bento intermediate step — between 768 and 1024 px the 2-col grid stretches cards to match the tallest sibling, leaving empty whitespace beneath the short ones. Single-col flex all the way untillg:.items-endon mobile (sm:items-center sm:justify-enddesktop, full-width Cancel + Delete buttons on mobile / auto width on md+) so the buttons sit right-aligned and easy to reach on touch instead of left-aligned against the modal edge.Closes #21 #22 #23 #24 #25 #26.
Commits (11, in order)
b24fba6fix(mobile-ui): wrap memory card values to prevent URL overflow (#21)ae7f71afix(mobile-ui): mobile observability tiles reflow + scrollable waterfall (#22)c8f5581fix(mobile-ui): show Settings entry in mobile user menu (#23)0f5e070fix(mobile-ui): skip chat composer autofocus on touch devices (#24)0410ff3fix(mobile-ui): landing bento cards stack on content height (#25)af831acfix(mobile-ui): memory delete modal stops stretching buttons edge-to-edge (#26)6f62277fix(mobile-ui): stack threads-summarize action buttons vertically on mobile (#21 follow-up)07f8f2dfix(mobile-ui): drop sticky + cap waterfall columns + hide tooltip on mobile (#22 follow-up)9067a63fix(mobile-ui): kill md bento intermediate step on landing (#25 follow-up)0be7260fix(mobile-ui): move About-you Delete below value + reorder thread-summarize actions (#21 v3)da3f043fix(mobile-ui) #21 v4 + #26 v2: full-width Delete below value, Expand+Delete grouped, dialog footer right-aligned + tests0c5f051fix(mobile-ui): observability v5 — stat tile polish for narrow viewportsHow to test
git checkout fix/mobile-ui-batch1 pnpm dev # if not already running, reuse the session on :3000Then in Chrome DevTools → toggle device toolbar → iPhone 14 (390×844, hasTouch):
/settings/memorywith a long-value row → URL wraps inside the card; a thread summary row shows Expand then SummaryContent then Delete stacked full-width (or click Delete directly to open the dialog)./chatfresh → soft keyboard stays hidden; chat history visible./→ scroll into#features; cards stacked single-column with content-height; no inflated whitespace. At iPad portrait (~768 px) the cards still stack — the asymmetric bento only appears at iPad landscape / desktop (~1024 px+).Verification status
Captured via Playwright + iPhone 14 emulation against the running dev server with a real session cookie. Screenshots at
.claude/screenshots/verify-{21..26}.png, all MD5s unique each run..wrap-break-wordcells renderoverflow-wrap: break-word;scrollW === docW === 390(no horizontal overflow). About-you store Delete hascol-span-2 w-full md:col-span-1 md:w-auto(full row below value on mobile). Thread-summarize storesSummaryContentelement and renders it twice (verified by test:data-slot="thread-body"count === 2 with mobilemd:hidden+ desktophidden md:blockancestors).overflow-x: auto. Follow-up:topCardSticky=false(top header not sticky on mobile),tooltipsVisible=0(hover popup hidden viahidden md:block). v5 polish: per-cardline-clamp-1+text-ellipsisclips long labels;token→tokkeeps the value on one line at 4-col phone width.[role='menuitem']items =["Memory", "Settings", "Sign Out"]on mobile./chatload →document.activeElement === <body>; composer unfocused.height ≈ scrollHeight ± 2px. Follow-up commit9067a63collapses the md bento step — desktop (≥ lg) retains the asymmetric grid, everything below lg is single-column flex.left=24px right=24px; footeralign-items: flex-end,flex-direction: column-reverse; buttons full-width on mobile (Cancel 292 px + Delete 292 px), reverting to intrinsic width on md+. Both profile and thread dialogs covered (asserted viadata-slot="dialog-footer"class + Cancel/Delete button class assertions).Tests added (6 new, 3 existing updated)
tests/frontend/settings/memory-view.test.tsx:mobile (#21): Profile store Delete is full-width + col-span-2— pinned by className inspection on every Profile-store Delete button (w-full,col-span-2,md:col-span-1,md:w-auto).mobile (#21): Thread-summarize Delete is full-width on mobile + w-auto at md+— className on thedata-slot="thread-collapse"row's Delete.mobile (#21): Thread body rendered twice (mobile + desktop)— countsdata-slot="thread-body"elements per thread (2 per row) and checks each copy is wrapped in the right responsive ancestor (.md:hiddenfor mobile,.hidden.md:blockfor desktop).mobile (#26): Dialog footer is right-aligned on mobile (items-end)—[data-slot="dialog-footer"]must containitems-endand notitems-start; opens the profile-delete dialog before checking.mobile (#26): Profile dialog Cancel + Delete are full-width on mobile + auto on md+— same className pattern on both buttons, dialog open.mobile (#26): Thread dialog Cancel + Delete are full-width on mobile + auto on md+— same check, thread-dialog open.Existing tests that depended on
getByText(/intro question/)/getByText(/Summary · N/)were switched toqueryAllByText(...).length >= 1because the newSummaryContentrendered twice (mobile + desktop) makes the same Q&A text show up in two DOM slots in jsdom. The parent<Collapsible open>keeps both copies coordinated.Caveats
save_memorytool call over chat (Please call save_memory with favoriteUrl=<long URL>); the dialog flow would otherwise have no row with a long value. The CSS rule itself is class-based and applies to every memory row regardless of value content.SummaryContent × 2test plus the action-row column-class assertions. To eyeball it visually, drop a thread summary into the user's store and reload/settings/memory.wrap-break-word, which compiles tooverflow-wrap: break-word— notanywhere. Both prevent URL overflow visually;break-wordonly breaks long words that would otherwise overflow, whileanywherebreaks anywhere and also affects the element'smin-contentsize. The visual outcome is identical here. If we ever want min-content to shrink with the URL width, upgrade to awrap-anywhererule.LABEL_WIDTHis still 240 px in the layout context (used to computecontentWidth), but every visible label/bar cell now hasmax-w-[50%] md:max-w-none+overflow-hiddenon mobile. Content math remains the same — the visual cap is pure CSS.Skipped (out of scope)
Triaged but moved to follow-up PRs:
add()-side async upload), not a CSS / event-gate change. See [[sdk-attachment-progress-add-only]].Files changed
components/settings/memory-view.tsxb24fba6,6f62277,0be7260,da3f043components/observability/panel.tsxae7f71a,07f8f2dapp/assistant.tsxc8f5581components/assistant-ui/thread.tsx0f5e070components/landing/features.tsx0410ff3,9067a63components/ui/dialog.tsxaf831ac,da3f043tests/frontend/settings/memory-view.test.tsxda3f043Total: 7 files, 10 commits, +209 −92 (final
git diff --stat main..fix/mobile-ui-batch1).