Skip to content

Fix mobile chat shortcut scroll edge blur - #7051

Merged
azooz2003-bit merged 4 commits into
mainfrom
fix-ios-bottom-scroll-blur
Jun 30, 2026
Merged

azooz2003-bit merged 4 commits into
mainfrom
fix-ios-bottom-scroll-blur

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jun 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes the iOS chat composer shortcut row edge treatment so horizontally scrolling glass chips blur out before they pass under pinned controls or the screen edge.

The old row used an alpha mask on the scroll view. With glass chips over glass chrome, that left a sharp glass-on-glass cutoff. The new row measures scroll content and viewport width, renders material blur curtains only when the row actually overflows, and keeps those curtains non-interactive so shortcuts remain tappable.

Validation

  • swift test --package-path Packages/iOS/CmuxAgentChatUI
  • ./scripts/reload-cloud.sh --tag blur (fleet unavailable locally, fell back to tagged local ./scripts/reload.sh --tag blur)
  • ./ios/scripts/reload.sh --tag blur on iPhone 17 simulator
  • Launched dev.cmux.ios.blur with CMUX_UITEST_AGENT_CHAT_INLINE_PREVIEW=1 and captured /tmp/cmux-blur-proof/chat-preview-final.png to verify the chat composer rendered and the non-overflow case no longer shows a dark edge block

Notes

The local Next.js helper server was started in a visible cmux helper terminal on port 9330. / warmed with 200. /handler/sign-in returns 404 in this checkout without Stack auth secrets; /handler/after-sign-in returns 307. Physical iPhone reload was unavailable because devicectl listed all paired devices as unavailable.


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


Summary by cubic

Fixes the iOS chat shortcut row so horizontally scrolling chips fade at the edges only when the row overflows, and keeps chips outside the fade and tappable.

  • Bug Fixes

    • Detects overflow by measuring scroll content vs. viewport width with onGeometryChange.
    • Replaces the fixed mask with a conditional gradient mask (34pt edges) that activates only on overflow, and insets content by 34pt so chips never sit under the fade.
  • New Features

    • Adds inline workspace demo shortcuts in AgentChatDemoScreen using CmuxMobileTerminal actions to preview scrolling and the edge fade.
    • Maps Escape/Ctrl+C to soft/hard interrupt in the demo, and adds leading “Hide Keyboard” and “Terminal” controls.

Written for commit f474e4a. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved the horizontal edge fade for chat accessory chips, so the fade appears only when content actually overflows and scrolling is needed.
    • Refined overflow handling in the chip row for smoother, more consistent scrolling visuals.
  • New Features
    • Enhanced the agent chat demo preview with terminal input accessory shortcuts, including preview handling for interrupt actions.

@vercel

vercel Bot commented Jun 29, 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 Jun 30, 2026 5:53am
cmux-staging Building Building Preview, Comment Jun 30, 2026 5:53am

@coderabbitai

coderabbitai Bot commented Jun 29, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

ChatAccessoryChipRow now decides its edge fade from measured content and viewport widths. AgentChatDemoScreen adds terminal shortcut support for inline workspace chat screens and maps selected preview shortcuts to interrupt actions.

Scroll Edge Fade Rework

Layer / File(s) Summary
Geometry-driven fade gating
Packages/iOS/.../ChatAccessoryChipRow.swift
Adds width state, geometry handlers, and the overflow check that decides when the edge fade is enabled.
Conditional edge fade mask
Packages/iOS/.../ChatAccessoryChipRow.swift
Replaces the direct gradient mask with a helper that returns either a full opaque mask or a three-part directional fade mask.

Demo Chat Shortcuts

Layer / File(s) Summary
Chat screen shortcut wiring
Packages/iOS/.../AgentChatDemoScreen.swift
Adds the terminal import and updates baseChatScreen(for:) so inline workspace screens receive leading and scrollable shortcut lists.
Preview shortcut actions
Packages/iOS/.../AgentChatDemoScreen.swift
Builds leading and scrollable preview shortcuts from terminal actions and routes shortcut selection to interrupt calls.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 The chips now fade by measured light,
And demo shortcuts hop just right.
One screen listens, one scrolls with care,
Soft little bunnies everywhere.


Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
Cmux Architecture Rethink ❌ Error AgentChatDemoScreen adds a separate shortcut router but only handles .escape/.ctrlC, leaving other rendered chips tappable yet inert. Route the demo through the same shared shortcut/action path as production, or restrict the preview row to actions it can actually execute.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (23 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: adjusting the mobile chat shortcut row's edge blur behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed Changed code stays in SwiftUI views; ChatConversationStore is @MainActor, and the new Task/interrupt pattern matches existing UI code, so no new isolation debt.
Cmux Swift Blocking Runtime ✅ Passed No new blocking primitives: the Swift changes only add geometry state and async Task wrappers; no semaphores, sleeps, sync waits, polling, or locks were introduced.
Cmux Browser Automation Off-Main ✅ Passed PR only changes iOS chat UI files; it doesn’t touch Sources/TerminalController.swift or ControlCommandExecutionPolicy.swift, so the browser-automation wait rule isn’t implicated.
Cmux Expensive Synchronous Load ✅ Passed The changed files only adjust UI geometry/masking and debug preview shortcuts; no synchronous agent-history loads, JSON scans, or hook-store access were added.
Cmux Cache Substitution Correctness ✅ Passed The diff only changes UI geometry-driven fades and demo shortcut wiring; no authoritative read in a persistence/history/undo/snapshot path was replaced by a cache.
Cmux No Hacky Sleeps ✅ Passed Only two Swift files changed; this rule covers non-Swift runtime scripts, so there’s no hacky-sleep violation in scope.
Cmux Algorithmic Complexity ✅ Passed PASS: the new scans are over a fixed enum-backed action list and tiny shortcut arrays; no nested rescans or user-scale hot-path filtering were added.
Cmux Swift Concurrency ✅ Passed Only new async use is Task-bridged SwiftUI button actions calling async interrupt(); no DispatchQueue, Combine, or completion-handler patterns were added.
Cmux Swift @Concurrent ✅ Passed No changed Swift code misuses @concurrent or nonisolated async; the new interrupt calls hop via Task to @MainActor store APIs, and the rest is UI-bound sync work.
Cmux Swift File And Package Boundaries ✅ Passed PASS: The touched Swift files are small (202/175 lines), UI/debug-only, and the new geometry-state logic stays in focused view glue, not a package-boundary violation.
Cmux Swiftpm Lockfiles ✅ Passed The PR only changes two Swift source files; no .gitignore, Package.resolved, Package.swift, or xcodeproj package-reference files are in scope.
Cmux Swift Logging ✅ Passed No diff-added Swift logging APIs or ad hoc diagnostics were present in the changed files.
Cmux User-Facing Error Privacy ✅ Passed The diff only changes chip-row layout and debug demo shortcuts; it adds no user-facing errors, alerts, or recovery copy, and the demo code is DEBUG-only.
Cmux Full Internationalization ✅ Passed Changed text is confined to #if DEBUG demo UI; the production chip-row change adds no user-facing strings, and no catalogs/messages were altered.
Cmux Swiftui State Layout ✅ Passed No forbidden pattern added: both views use @State with onGeometryChange callbacks, no new ObservableObject/@published, no lazy-list store refs, and no render-time writes.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Touched files are SwiftUI views only; no NSWindow/WindowGroup/NSPanel, window identifiers, or cmuxAuxiliaryWindowIdentifiers changes, so the rule doesn’t apply.
Cmux Source Artifacts ✅ Passed The changed paths are hand-written Swift source under package Sources, and no logs, screenshots, temp, cache, or build artifacts were added.
Cmux No Test Or Debug Seam In Production Source ✅ Passed No new test/debug seam was added: the only DEBUG-gated file is a demo host, and the production row changes just add private geometry state and masking logic.
Cmux No Ambient Global State ✅ Passed Both changed files define only scoped members inside structs; no new file-scope func/var or singleton-like state appears in the diff.
Description check ✅ Passed Summary and validation are detailed and align with the template; only optional sections like demo video and checklist are missing.
✨ 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 fix-ios-bottom-scroll-blur

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 29, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes the iOS chat composer shortcut row's edge fade by replacing a fixed alpha mask with a measurement-driven conditional gradient, ensuring the blur only activates when chips overflow the viewport and that chips at rest are not obscured.

  • ChatAccessoryChipRow: Adds @State tracking for scrollContentWidth and scrollViewportWidth via onGeometryChange. When overflow is detected (scrollContentWidth > scrollViewportWidth + 1), a 34pt leading + trailing gradient mask activates and matching 34pt horizontal padding is applied to the scroll content — so at scroll-offset 0 the first chip lands exactly at the gradient's fully-opaque boundary, resolving both the false-positive leading blur and the glass-on-glass cutoff.
  • AgentChatDemoScreen (#if DEBUG): Adds inline-workspace preview shortcuts built from TerminalInputAccessoryAction.defaultConfigurableOrder, mapping Escape/Ctrl+C to soft/hard interrupt so the overflow and edge-fade behaviour can be validated in the simulator without a Mac host.

Confidence Score: 5/5

Safe to merge. Changes are scoped to a visual scroll-edge treatment in one SwiftUI chip row and a debug-only demo screen.

The geometry measurement ordering is correct: onGeometryChange is applied before .padding in the modifier chain, so scrollContentWidth captures raw chip width and the comparison is accurate. The 34pt padding precisely matches the 34pt gradient width, so chip edges align with gradient boundaries at both scroll extents. The fade state is stable (onGeometryChange converges in two render passes with no oscillation). The AgentChatDemoScreen additions are entirely behind #if DEBUG and don't touch any production path.

No files require special attention.

Important Files Changed

Filename Overview
Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Composer/ChatAccessoryChipRow.swift Replaces the fixed LinearGradient mask with a measurement-based conditional gradient. Uses onGeometryChange correctly (before .padding in the modifier chain, so scrollContentWidth reflects raw chip width only). The 34pt padding matches the 34pt fade width, ensuring chip alignment at both ends of the scroll range.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentChatDemoScreen.swift Entirely guarded by #if DEBUG. Adds previewLeadingShortcuts and previewScrollableShortcuts within the existing demo screen to drive the inlineWorkspace style — no production behaviour is changed.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[View renders\nscrollContentWidth=0\nscrollViewportWidth=0] --> B[onGeometryChange fires\nfor HStack content]
    A --> C[onGeometryChange fires\nfor ScrollView viewport]
    B --> D{scrollContentWidth >\nscrollViewportWidth + 1?}
    C --> D
    D -- No --> E[padding = 2pt\nmask = plain Rectangle]
    D -- Yes --> F[padding = 34pt\nmask = leading + trailing\nLinearGradient curtains]
    F --> G[At offset 0:\nfirst chip left-edge lands\nat gradient fully-opaque boundary\n→ chip fully visible]
    F --> H[At max offset:\nlast chip right-edge aligns\nwith trailing gradient start\n→ chip fully visible]
    E --> I[No fade artifacts\non short lists]
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[View renders\nscrollContentWidth=0\nscrollViewportWidth=0] --> B[onGeometryChange fires\nfor HStack content]
    A --> C[onGeometryChange fires\nfor ScrollView viewport]
    B --> D{scrollContentWidth >\nscrollViewportWidth + 1?}
    C --> D
    D -- No --> E[padding = 2pt\nmask = plain Rectangle]
    D -- Yes --> F[padding = 34pt\nmask = leading + trailing\nLinearGradient curtains]
    F --> G[At offset 0:\nfirst chip left-edge lands\nat gradient fully-opaque boundary\n→ chip fully visible]
    F --> H[At max offset:\nlast chip right-edge aligns\nwith trailing gradient start\n→ chip fully visible]
    E --> I[No fade artifacts\non short lists]
Loading

Reviews (3): Last reviewed commit: "Keep shortcut chips outside edge fade" | Re-trigger Greptile

Comment on lines 70 to 79
.overlay(alignment: .leading) {
if scrollNeedsEdgeBlur {
scrollEdgeBlur(isLeading: true)
}
}
.overlay(alignment: .trailing) {
if scrollNeedsEdgeBlur {
scrollEdgeBlur(isLeading: false)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Leading blur visible at scroll position 0

Both curtains appear whenever scrollNeedsEdgeBlur is true, but at the initial scroll position (0) there is no content hidden behind the leading edge — the first chip is right at the left boundary. The 34pt leading curtain will overlap and visually obscure the first chip(s) for every overflowing row before the user has scrolled right at all, which is the common case. Only the trailing curtain is correct at position 0; the leading one should only appear once scrollOffset > 0. Tracking scroll position (e.g. via ScrollPosition on iOS 17+, or a preference-key offset reader) is needed to gate isLeading: true independently of isLeading: false.

Comment on lines +109 to +111
private var scrollNeedsEdgeBlur: Bool {
scrollContentWidth > scrollViewportWidth + 1
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 The scrollContentWidth measurement is taken on the view after .padding(.horizontal, 2) is applied, so it is always 4pt wider than the raw chip content. When compared against scrollViewportWidth with a tolerance of only + 1, this means the blur curtains can appear even when the chips fit the viewport exactly (4pt padding > 1pt tolerance). Subtracting the padding from the content measurement corrects the comparison.

Suggested change
private var scrollNeedsEdgeBlur: Bool {
scrollContentWidth > scrollViewportWidth + 1
}
private var scrollNeedsEdgeBlur: Bool {
// scrollContentWidth is measured on the padded HStack (2pt on each side = 4pt total),
// so subtract the padding before comparing to the bare viewport width.
scrollContentWidth - 4 > scrollViewportWidth + 1
}

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@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/AgentChatDemoScreen.swift`:
- Around line 116-168: The preview accessory chips are exposing unsupported
actions as clickable no-ops. Update AgentChatDemoScreen’s
previewLeadingShortcuts and previewScrollableShortcuts/performPreviewShortcut so
only actions that actually work in this screen are rendered, or wire the
remaining TerminalInputAccessoryAction cases through the same input handling
path used by the real accessory; in particular, avoid generating chips whose
closure does nothing and ensure each shown ChatAccessoryShortcut maps to a
handled action.
🪄 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: fd7a4860-17a0-43d3-9ce4-b28bd782cf6c

📥 Commits

Reviewing files that changed from the base of the PR and between e857082 and 07aa5f4.

📒 Files selected for processing (2)
  • Packages/iOS/CmuxAgentChatUI/Sources/CmuxAgentChatUI/Composer/ChatAccessoryChipRow.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentChatDemoScreen.swift

Comment on lines +116 to +168
private var previewLeadingShortcuts: [ChatAccessoryShortcut] {
[
ChatAccessoryShortcut(
id: "terminal.inputAccessory.hideKeyboard",
title: "",
systemImage: "keyboard.chevron.compact.down",
accessibilityLabel: L10n.string(
"terminal.input_accessory.hideKeyboard",
defaultValue: "Hide Keyboard"
),
tint: .secondary,
semanticAction: .dismissKeyboard
) {},
ChatAccessoryShortcut(
id: "terminal.inputAccessory.composer",
title: "",
systemImage: "terminal",
accessibilityLabel: L10n.string(
"mobile.terminal.select",
defaultValue: "Terminal"
)
) {},
]
}

private func previewScrollableShortcuts(for stack: DemoStack) -> [ChatAccessoryShortcut] {
TerminalInputAccessoryAction.defaultConfigurableOrder.compactMap { action in
guard action.isSupportedInAgentChat else { return nil }
return ChatAccessoryShortcut(
id: action.accessibilityIdentifier,
title: action.title(isMacRemote: true),
systemImage: action.symbolName,
accessibilityLabel: action.accessibilityLabel ?? action.settingsDisplayName,
semanticAction: action == .paste ? .paste : nil
) {
performPreviewShortcut(action, store: stack.store)
}
}
}

private func performPreviewShortcut(
_ action: TerminalInputAccessoryAction,
store: ChatConversationStore
) {
switch action {
case .escape:
Task { await store.interrupt(hard: false) }
case .ctrlC:
Task { await store.interrupt(hard: true) }
default:
break
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Most of the new accessory chips are wired as no-ops.

previewLeadingShortcuts adds a Terminal chip with an empty action, and previewScrollableShortcuts builds chips for every agent-chat-supported terminal action even though performPreviewShortcut only handles .escape and .ctrlC. That means chips like tab/return/ctrlD/claude/codex/etc. are rendered as tappable but never do anything. Either limit this preview row to the actions this screen actually supports, or route the remaining actions through the same input path as the real accessory.

🤖 Prompt for 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.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentChatDemoScreen.swift`
around lines 116 - 168, The preview accessory chips are exposing unsupported
actions as clickable no-ops. Update AgentChatDemoScreen’s
previewLeadingShortcuts and previewScrollableShortcuts/performPreviewShortcut so
only actions that actually work in this screen are rendered, or wire the
remaining TerminalInputAccessoryAction cases through the same input handling
path used by the real accessory; in particular, avoid generating chips whose
closure does nothing and ensure each shown ChatAccessoryShortcut maps to a
handled action.

…os-bottom-scroll-blur

# Conflicts:
#	Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/AgentChatDemoScreen.swift
@azooz2003-bit
azooz2003-bit merged commit 335ebf6 into main Jun 30, 2026
36 checks passed

This branch was successfully deployed

1 active deployment
Preview – cmux — f474e4a4 Deployed Jun 30, 2026 by vercel[bot]
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