Skip to content

iOS: nest composer attachment preview inside the field bubble (iMessage-style) - #6813

Closed
austinywang wants to merge 12 commits into
mainfrom
issue-6643-ios-attached-image-preview-is-misaligned
Closed

austinywang wants to merge 12 commits into
mainfrom
issue-6643-ios-attached-image-preview-is-misaligned

Conversation

@austinywang

@austinywang austinywang commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #6643.

Problem

On the iOS app, attaching an image to the terminal composer rendered the thumbnail chip as a detached row above the rounded glass field, anchored only by an ad-hoc .padding(.leading, controlHeight + 8) = 48pt. That inset matched neither the paperclip column (12pt) nor the field/text column (~108pt), so the thumbnail floated above the gap between the paperclip and mic buttons and read as a stray square rather than part of the composer.

Fix

Match iMessage: render the staged thumbnail(s) inside the same rounded .mobileGlassField container as the text field — stacked above the text, sharing the field's true leading text inset. The bubble grows to wrap the image and the send button stays pinned bottom-trailing.

  • MobileComposerFieldContainer (the shared shell used by both the terminal composer and GUI chat) gains an optional header slot rendered inside the glass, above the field row, at the field's 14pt leading inset. A two-closure convenience initializer (Header == EmptyView) keeps every existing call site (ChatComposerView) compiling and laying out exactly as before — no behavior change for callers that pass no header.
  • TerminalComposerView.composerBar moves the attachment chip row from a sibling above the input HStack into the field container's header, dropping the hand-tuned 48pt leading inset. The chip row hugs its content height via fixedSize(vertical:) so the bubble grows by exactly the chip row height (not greedily).

Scope / testing

  • Layout-only change. No behavior changes, no string changes (localization audit: all composer strings remain L10n.string calls, none added or changed).
  • Not cleanly unit-testable — SwiftUI layout/alignment has no snapshot-test infrastructure in this repo for the composer. Verified by syntax parse and code review; visual confirmation via a tagged build.
  • Follow-up opportunity (not in this PR, since iOS: attached image preview is misaligned in the composer — should match iMessage #6643 is scoped to the terminal composer): ChatComposerView's attachmentStrip has a related floating-chip layout and can now adopt the same shared header slot.

Demo Video

Visual verification was performed for the original UI fix from the tagged debug build noted in the PR description. No public demo video is attached because this is an iOS app composer alignment fix.

Review Trigger

  • Latest trigger: pushed the current head and reran $autoreview --mode branch.
  • Review bots: CodeRabbit, Greptile, Cubic, Socket, Vercel, and CI status checks are tracked on this PR.

Checklist

  • Linked issue included.
  • Scope limited to iOS composer layout.
  • No user-facing strings added or changed.
  • CI is the merge gate.
  • No local app build or launch was run during PR closeout.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Nest attachment thumbnails inside the rounded mobileGlassField bubble in the iOS terminal composer (iMessage-style) to fix the floating chip row in #6643. The bubble expands to fit previews; send stays bottom-trailing, and the composer remeasures when the attachment count changes.

  • Bug Fixes

    • Move the attachment strip into the field container header; drop the 48pt manual inset.
    • Nest chips at the field’s 14pt leading inset and use fixedSize(vertical:) so the bubble grows only by the chip height.
    • Remeasure the composer band from pendingAttachments.count to cover all staging paths.
  • Refactors

    • Add an optional header slot to MobileComposerFieldContainer, rendered inside the glass above the field; include a header-less convenience initializer with DocC. Existing layouts are unchanged.

Written for commit 05901c0. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • The message composer now supports a more compact layout, with attachment chips nested inside the composer bubble.
    • Attachment, microphone, and send controls are repositioned for a cleaner, more consistent input area.
  • Bug Fixes

    • Attachment chips now size more naturally instead of expanding the composer vertically.

…ge-style)

Fixes #6643.

When an image is attached to the iOS terminal composer, the thumbnail chip
rendered as a detached row ABOVE the rounded glass field, anchored only by an
ad-hoc `.padding(.leading, controlHeight + 8)` (48pt). That inset matched
neither the paperclip column (12pt) nor the field/text column (~108pt), so the
thumbnail floated above the gap between the paperclip and mic buttons and read
as a stray square.

Match iMessage: render the staged thumbnails INSIDE the same rounded
`.mobileGlassField` container as the text field, stacked above the text and
sharing the field's leading text inset. The bubble grows to wrap the image and
the send button stays pinned bottom-trailing.

- `MobileComposerFieldContainer` (shared by the terminal composer and GUI chat)
  gains an optional `header` slot rendered inside the glass, above the field row,
  at the field's 14pt leading inset. A two-closure convenience initializer
  (`Header == EmptyView`) keeps every existing call site (ChatComposerView)
  compiling and laying out exactly as before.
- `TerminalComposerView.composerBar` moves the attachment chip row from a sibling
  above the input HStack into the field container's header, dropping the
  hand-tuned 48pt leading inset. The chip row hugs its content height via
  `fixedSize(vertical:)` so the bubble grows by exactly the chip row.

Layout-only change (no behavior or string changes); not cleanly unit-testable
without composer snapshot infrastructure, which the repo does not have.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Jul 5, 2026 2:26am
cmux-staging Building Building Preview, Comment Jul 5, 2026 2:26am

@coderabbitai

coderabbitai Bot commented Jun 26, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The iOS composer now uses a container that can render a header above the text field row. TerminalComposerView passes attachment chips into that header, repositions the attach/mic controls, and constrains the chip row’s vertical sizing.

Changes

Composer bubble layout

Layer / File(s) Summary
Header-aware field container
Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/MobileComposerFieldContainer.swift
MobileComposerFieldContainer adds a header view above the field/trailing row and keeps a headerless convenience initializer matching the previous call shape.
Composer bar attachment nesting
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift
composerBar passes attachment chips into the container header, moves attach/mic controls beside the container, routes the send button through the trailing closure, and applies fixed vertical sizing to the chip row.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • manaflow-ai/cmux#6102: Adds pending image attachments and attachment chips in the composer UI, which overlaps with the new header-based chip nesting here.

Poem

I tucked a chip inside the glow,
where soft round bubbles gently grow.
The buttons hop in tidy place,
and send still waits in its bright space.
🐇✨

🚥 Pre-merge checks | ✅ 25
✅ Passed checks (25 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes directly address #6643 by nesting the attachment preview inside the composer bubble and preserving send-button placement.
Out of Scope Changes check ✅ Passed No unrelated changes are evident; the shared container refactor supports the requested composer layout change.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Cmux Swift Actor Isolation ✅ Passed UI-only SwiftUI layout changes; CMUXMobileShellStore is @MainActor and no new Sendable/reference or background-store access was introduced.
Cmux Swift Blocking Runtime ✅ Passed PASS: The Swift changes are layout-only; the touched files add no semaphores, waits, sleeps, asyncAfter, main-sync, or manual locks.
Cmux Browser Automation Off-Main ✅ Passed PASS: diff only bumps submodules and adds terminal selection/clipboard C APIs in ghostty; no browser socket command, routing, or policy/test files changed.
Cmux Expensive Synchronous Load ✅ Passed The diff only refactors SwiftUI composer layout; it adds no agent-history/session/transcript loads, JSON parsing, or filesystem scans on MainActor/interactive paths.
Cmux Cache Substitution Correctness ✅ Passed PASS: the new thumbnail cache is used only for transient composer UI rendering; send/persistence paths still read attachments from the store, not the cache.
Cmux No Hacky Sleeps ✅ Passed No fixed sleeps, timers, asyncAfter, polling, or retry delays appear in the changed runtime files; package/workflow edits are script/version tweaks only.
Cmux Algorithmic Complexity ✅ Passed No unbounded rescans: attachment work is capped at 10 items, and the new reduce/map calls run only over that tiny fixed-size collection.
Cmux Swift Concurrency ✅ Passed Touched files are layout-only; no new DispatchQueue, Combine, completion-handler, or fire-and-forget Task patterns were added. Existing Task usage is structured/cancelled.
Cmux Swift @Concurrent ✅ Passed Layout-only SwiftUI refactor; no new or changed @concurrent/nonisolated async isolation issues or heavy async UI call sites in the touched files.
Cmux Swift File And Package Boundaries ✅ Passed Focused UI/layout fix: an 80-line shared support helper plus a small edit to an already-oversized 818-line TerminalComposerView; no new mixed responsibilities or boundary violations.
Cmux Swiftpm Lockfiles ✅ Passed Diff only changes iOS layout, one workflow command, and Bun files; no cmux SwiftPM lockfile, .gitignore, or Xcode package-reference policy issue appears.
Cmux Swift Logging ✅ Passed No print/debugPrint/dump/NSLog, ad hoc stdout logging, or bad Logger declarations were added or changed in the touched Swift files.
Cmux User-Facing Error Privacy ✅ Passed Touched files only add layout code and safe UI labels; no user-facing error, alert, recovery, or raw diagnostic copy exposing sensitive details.
Cmux Full Internationalization ✅ Passed Diff is layout-only: touched Swift files add no new user-facing text, reuse existing L10n labels, and no string catalogs/Info.plist locales were changed.
Cmux Swiftui State Layout ✅ Passed PASS: The diff only refactors composer layout; no new ObservableObject/@published, GeometryReader measurement, lazy-row store refs, or render-time state writes in changed views.
Cmux Architecture Rethink ✅ Passed Layout-only refactor adds a header slot in the shared field container; no sleeps, observers, duplicate entrypoints, or split ownership were introduced.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Touched files only refactor SwiftUI composer views; no NSWindow/WindowGroup/close-shortcut code was added or changed.
Cmux Source Artifacts ✅ Passed Only changed paths are Swift source files; no logs, screenshots, caches, temp dirs, or generated artifact paths were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No new test-only or debug-seam API appears in the production Swift changes; the lone #if DEBUG is existing diagnostic logging, not a test hook.
Cmux No Ambient Global State ✅ Passed Changed files add only view-scoped structs/extensions and @State-owned helpers; no new file-scope mutable vars, singleton APIs, or ambient global state.
Title check ✅ Passed The title clearly matches the main change: nesting the composer attachment preview inside the field bubble on iOS.
Description check ✅ Passed The template sections are present and mostly complete, including summary, testing, demo video, review trigger, and checklist.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-6643-ios-attached-image-preview-is-misaligned

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jun 26, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the floating attachment chip row in the iOS terminal composer by nesting thumbnails inside the same rounded glass field container — matching iMessage's layout — instead of rendering them as a detached sibling view with a hand-tuned 48pt leading inset.

  • MobileComposerFieldContainer gains a generic Header slot (VStack above the field row) with a three-closure primary init; a Header == EmptyView extension init preserves all existing call sites (ChatComposerView) with zero layout change.
  • TerminalComposerView.composerBar moves the attachment chip row into the container's header: closure, drops the 48pt inset, and adds fixedSize(horizontal: false, vertical: true) so the horizontal ScrollView hugs its content height rather than expanding greedily inside the bubble.
  • A previously missing onChange(of: pendingAttachments.isEmpty) remeasure trigger is added, fixing the composer band staying reserved-tall after image-only sends that cleared attachments without touching terminalInputText.

Confidence Score: 5/5

Safe to merge — purely a layout reorganization with no behavioral, state, or API changes outside the composer surface.

All three files contain straightforward layout changes: the generic Header slot in MobileComposerFieldContainer is backward-compatible via a constrained extension initializer, the fixedSize modifier and isEmpty onChange are both standard SwiftUI patterns applied correctly, and no logic paths, concurrency, persistence, or external APIs are touched.

No files require special attention.

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/MobileComposerFieldContainer.swift Adds an optional header generic slot (VStack above the field row) with a three-closure primary init and a Header==EmptyView extension that preserves all existing call sites exactly; no logic, state, or behavior changes.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift Moves the attachment chip row from a sibling VStack above the glass bubble into MobileComposerFieldContainer's header slot; adds a missing onChange(of: pendingAttachments.isEmpty) remeasure trigger that fixed the band staying tall after image-only sends; drops the hand-tuned 48pt leading inset and fixedSize(vertical:) is placed correctly on the horizontal ScrollView.
.github/swift-file-length-budget.tsv Tightens the GhosttySurfaceView.swift budget from 4283 to 3964 lines, reflecting the file's current line count after prior reductions; other entries unchanged.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[composerBar HStack / .bottom] --> B[MobileComposerIconButton paperclip]
    A --> C[micButton]
    A --> D[MobileComposerFieldContainer]

    D --> E[VStack / .leading / spacing 6]
    E --> F{pendingAttachments.isEmpty?}
    F -- false --> G[attachmentChipRow ScrollView horizontal fixedSize vertical true]
    F -- true --> H[EmptyView / collapses]
    E --> I[HStack / .bottom / spacing 8]
    I --> J[TextField multi-line grows 1-14 lines]
    I --> K[Send Button circle bottom-trailing]

    subgraph HeightRemeasure
        L[onChange terminalInputText] --> M[requestHeightRemeasure]
        N[onChange pendingAttachments.isEmpty] --> M
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[composerBar HStack / .bottom] --> B[MobileComposerIconButton paperclip]
    A --> C[micButton]
    A --> D[MobileComposerFieldContainer]

    D --> E[VStack / .leading / spacing 6]
    E --> F{pendingAttachments.isEmpty?}
    F -- false --> G[attachmentChipRow ScrollView horizontal fixedSize vertical true]
    F -- true --> H[EmptyView / collapses]
    E --> I[HStack / .bottom / spacing 8]
    I --> J[TextField multi-line grows 1-14 lines]
    I --> K[Send Button circle bottom-trailing]

    subgraph HeightRemeasure
        L[onChange terminalInputText] --> M[requestHeightRemeasure]
        N[onChange pendingAttachments.isEmpty] --> M
    end
Loading

Reviews (9): Last reviewed commit: "Merge remote-tracking branch 'origin/mai..." | Re-trigger Greptile

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift`:
- Around line 285-291: The composer height is driven by staged attachments in
TerminalComposerView, but requestHeightRemeasure() only runs from text and local
picker/remove/send actions. Add a change hook tied to pendingAttachments (or
their count/IDs) so any attachment added through the store, including
paste-driven staging, triggers a remeasure. Wire it in the same view where
attachmentChipRow is shown and keep the existing text-based remeasure paths
intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 73e4c982-2630-4735-86ed-beb417665280

📥 Commits

Reviewing files that changed from the base of the PR and between a4fb35c and 6a07751.

📒 Files selected for processing (2)
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift
  • Packages/iOS/CmuxMobileSupport/Sources/CmuxMobileSupport/MobileComposerFieldContainer.swift

@blacksmith-sh

This comment has been minimized.

cmux and others added 3 commits June 26, 2026 01:00
Satisfies the Aziz DocC policy: the public extension declaration needs a
triple-slash doc comment, not only its member initializer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drive the band re-measure off pendingAttachments.count (symmetric with the
existing terminalInputText height driver) so the bubble grows for any staging
route instead of relying on each mutation path to call requestHeightRemeasure().
Addresses CodeRabbit review feedback on #6813.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The added onChange comment pushed TerminalComposerView.swift to 827 lines (budget
825). Condense the comment; the file is back to 824, under budget, with no logic
change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed-image-preview-is-misaligned

# Conflicts:
#	Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/TerminalComposerView.swift
…ed-image-preview-is-misaligned

# Conflicts:
#	.github/swift-file-length-budget.tsv
@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview – cmux — 05901c04 Deployed Jul 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

iOS: attached image preview is misaligned in the composer — should match iMessage

3 participants