diff --git a/apps/mobile/DESIGN.md b/apps/mobile/DESIGN.md
index 771db82544..e59d25e115 100644
--- a/apps/mobile/DESIGN.md
+++ b/apps/mobile/DESIGN.md
@@ -166,10 +166,10 @@ The shell is set by [ADR-0211](../../docs/adr/0211-the-mobile-app-has-two-tabs-w
- Header: the system navigation header of the root native stack (expo-router `Stack`), so back, the swipe back and the iOS 26 header transitions are the platform's own. It has no bar fill, title text, subtitle or state dot. Leading, from the left: Back, then right beside it the title chip (the Bot's avatar and name, leading-aligned, never centred), which opens the Bot profile. Trailing: one round button for the Bot's computer. Back is the native back button; a plain back control replaces it only when the chat is the first screen of the stack, and returns to Chats.
- iOS: the header is transparent (`headerTransparent`) with no `headerStyle.backgroundColor`, so UIKit draws the bar; a clear colour would remove its material. The chip is a native bar item of type `custom` (`unstable_headerLeftItems`), and `headerTitle` is empty. The computer button, and the plain back control when the chat is the first screen, are native bar buttons with SF Symbols (`macwindow`, `chevron.left`), so UIKit tints them. The chip's text uses the system `label` colour, not `fg`, because the bar's glass picks its own light or dark look from what is under it and from Reduce Transparency; a fixed `fg` went white on a light grey item in dark mode with Reduce Transparency on. A zero-width `spacing` item separates the chip from Back so each gets its own glass shape: a round Back and a separate capsule. iOS 26 draws that Liquid Glass behind the bar items itself, so the items carry no `glassEffect` or fill of their own (that would draw a second, skinny capsule). The chip is capped so the computer button always fits. Before iOS 26 the bar uses the system chrome blur.
- - Android: the header floats over the messages like iOS's (`headerTransparent`, no bar fill, no shadow), with the same three shapes as Grok's chat header: a round Back, the title chip at the start (`headerTitleAlign: 'left'`) as its own capsule island, and a round computer button. Each shape uses the opaque-iOS fallback of the header buttons (`headerGlassFill`: frosted `raised` fill with a `raised` hairline), so the chip reads as an island, not a label in a bar. Back is always this round button (it replaces the native navigation icon; system and predictive back still work). The list pads its own top by the header height, and messages scroll under the header through the drawn top fade (see below). Android has no system scroll edge effect: Material 3 only tints the top app bar on scroll, and the View fading edge (`fadingEdgeLength`) fades content linearly at the scroll view's own bounds, so it cannot keep what passes under the floating chrome hidden.
+ - Android: the header floats over the messages like iOS's (`headerTransparent`, no bar fill, no shadow), with the same three shapes as Grok's chat header: a round Back, the title chip at the start (`headerTitleAlign: 'left'`) as its own capsule island, and a round computer button, each its own shape `space.x3` apart (the title wrapper's horizontal margin), never touching. Each shape uses the opaque-iOS fallback of the header buttons (`headerGlassFill`: frosted `raised` fill with a `raised` hairline), so the chip reads as an island, not a label in a bar. Back is always this round button (it replaces the native navigation icon; system and predictive back still work). The list pads its own top by the header height, and messages scroll under the header through the drawn top fade (see below). Android has no system scroll edge effect: Material 3 only tints the top app bar on scroll, and the View fading edge (`fadingEdgeLength`) fades content linearly at the scroll view's own bounds, so it cannot keep what passes under the floating chrome hidden.
- The chip holds, in one row, the Bot's avatar (28pt, round, no state ring) and its name in `body` strong; a group shows its mark and name. It is `theme.touch` tall with padding concentric with its corners. The Bot's state is read out in its accessibility label only. Pressing shows the `raised` pressed fill. Icons in the header are `fg`, never the Bot's accent.
- iOS: messages run under the transparent header and the status bar, and UIKit's scroll edge effect (left on `automatic`) softens them there. The list uses `contentInsetAdjustmentBehavior="automatic"`, so UIKit, not a manual padding, keeps the header's height free above the oldest message at rest; the edge effect needs that automatic inset. The edge effect and React Native Screens both find the content scroll view through the first-subview chain of the screen, so the message list is the first thing in that chain: a default or colour chat background is the frame's own `backgroundColor`, not a view in front of the list. An image background still needs a view behind the list, and with one the soft edge does not reach the list. UIKit also adds the home-indicator safe area to the bottom inset while the list reaches the screen bottom (keyboard closed); the list cancels it with a negative `contentInset.bottom`, because the composer's height already includes that padding. Without this, scrolling to the very end left 34pt more space below the newest message than the resting position. There is no painted top gradient. A transparent header dimmed the whole message list on iOS 26 while the list was inverted (a scaleY flip); the list now runs top to bottom, which this header depends on.
- - The list runs to the bottom of the screen, and the composer floats over it with no bar of its own; messages pass around and under its **+** button and field capsule. On iOS 26 with Reduce Transparency off both shapes are Liquid Glass, like iOS Messages; older iOS and Reduce Transparency give them an opaque `surface` fill with a `borderStrong` hairline, and Android gives the field the opaque `raised` fill, so on those nothing shows through where the Human types. The bottom of the screen fades out with the same light gradient over the composer and `edgeFade.bottom` above it. The list keeps that much space below the newest message, so at rest it sits just above the composer; scrolled up, messages pass behind the composer. Dragging the list dismisses the keyboard: interactively on iOS (the keyboard follows the finger once it reaches it), on drag on Android. On iOS and Android the bottom edge is a drawn mask layer between the list and the composer, like Grok's chat: nearly opaque (`edgeFade.cover`) under the composer's measured height, then the light `edgeFade` curve over `edgeFade.bottom` above it, painted in the chat's own background colour (`base` with an image background). Android uses the same mask under its floating header. iOS 26 hides UIKit's bottom scroll edge effect for the chat (`scrollEdgeEffects.bottom: 'hidden'`) and does not use a `UIScrollEdgeElementContainerInteraction` on the composer: UIKit picked the hard style there, an opaque band in a different colour from the list with a hairline across its top, at a height that did not follow a wrapped draft, and neither re-adding the interaction nor pinning the `soft` style fixed it. The top edge stays UIKit's automatic scroll edge effect. With the keyboard open the composer's pill sits `space.x3` above it: the keyboard already covers the home indicator, so the chat frame takes the composer's safe-area padding back out of the keyboard offset instead of counting it twice.
+ - The list runs to the bottom of the screen, and the composer floats over it with no bar of its own; messages pass around and under its **+** button and field capsule. On iOS 26 with Reduce Transparency off both shapes are Liquid Glass, like iOS Messages; older iOS and Reduce Transparency give them an opaque `surface` fill with a `borderStrong` hairline, and Android gives the field the opaque `raised` fill, so on those nothing shows through where the Human types. The bottom of the screen fades out with the same light gradient over the composer and `edgeFade.bottom` above it. The list keeps that much space below the newest message, so at rest it sits just above the composer; scrolled up, messages pass behind the composer. Dragging the list dismisses the keyboard: interactively on iOS (the keyboard follows the finger once it reaches it), on drag on Android. On iOS and Android the bottom edge is a drawn mask layer between the list and the composer, like Grok's chat: one soft curve (`(1 − d)^edgeFade.curve`, sampled `edgeFade.samples` times) from fully opaque at the screen edge (held solid across the home-indicator or status-bar safe area, so nothing shows through in that strip on a coloured background) to clear `edgeFade.reach` above the composer's measured height, so it is nearly opaque only close to the screen edge and never a hard band at the composer's top, painted in the chat's own background colour (`base` with an image background). Android uses the same mask under its floating header. iOS 26 hides UIKit's bottom scroll edge effect for the chat (`scrollEdgeEffects.bottom: 'hidden'`) and does not use a `UIScrollEdgeElementContainerInteraction` on the composer: UIKit picked the hard style there, an opaque band in a different colour from the list with a hairline across its top, at a height that did not follow a wrapped draft, and neither re-adding the interaction nor pinning the `soft` style fixed it. The top edge stays UIKit's automatic scroll edge effect. With the keyboard open the composer's pill sits `space.x3` above it: the keyboard already covers the home indicator, so the chat frame takes the composer's safe-area padding back out of the keyboard offset instead of counting it twice.
- Messages:
- Activity rows show Bot DMs, posts elsewhere, memory commits, Bot creation, departures and stopped turns as one left-aligned meta line with a kind icon and tail ellipsis. They use the same `bubbleRow(true)` inset and single `space.x4` (12pt) top gap as a new message group, with no bottom padding or vertically centred minimum-height box: bubble → activity, activity → activity and activity → new message group keep the same boundary gap. Icons and avatars match the meta line height. In groups, the actor avatar occupies the existing speaker gutter, followed by the icon at the bubble's left edge and the description. In a Human DM, omit the current Bot's avatar and redundant name; Bot-DM actions show the other Bot's avatar after the icon. Missing avatars use the person icon.
- Activity rows do not navigate on tap. Long-press (or the accessibility details action) opens the message sheet with the complete plain-text content in a scrollable, selectable block capped at `messageSheet.textHeightRatio` of the window height, then Copy/Share and an explicit Open conversation, Open memory or Open new Bot action when a destination exists; there are no reactions. Consecutive posts by the same Bot into the same Channel on the same day fold into a counted row whose menu opens the latest post. Memory opens the Bot's memory files, not a commit diff. Bot-to-Bot DMs remain read-only. Keep full actor/action/detail text in the accessibility label and menu even when the compact memory line uses only the icon and commit subject.
diff --git a/apps/mobile/src/ui/chat/conversation-screen.tsx b/apps/mobile/src/ui/chat/conversation-screen.tsx
index c9d708d3d9..aeaf18668b 100644
--- a/apps/mobile/src/ui/chat/conversation-screen.tsx
+++ b/apps/mobile/src/ui/chat/conversation-screen.tsx
@@ -882,7 +882,10 @@ function ChatConversation({
headerTitle: () => (
{title}
@@ -1111,14 +1114,14 @@ function ChatConversation({
{listTop > 0 ? (
) : null}
diff --git a/apps/mobile/src/ui/chat/edge-fade.ts b/apps/mobile/src/ui/chat/edge-fade.ts
index 2528c301bd..d6998b4191 100644
--- a/apps/mobile/src/ui/chat/edge-fade.ts
+++ b/apps/mobile/src/ui/chat/edge-fade.ts
@@ -3,15 +3,15 @@ import { edgeFade } from '../design/tokens';
export function edgeFadeStops(
height: number,
cover: number,
+ solid = 0,
): readonly { offset: number; opacity: number }[] {
if (cover <= 0 || height <= cover) return edgeFade.stops;
- const start = cover / height;
- const peak = edgeFade.stops[0].opacity;
+ const start = Math.min(solid, cover) / height;
return [
- { offset: 0, opacity: 1 },
- ...edgeFade.stops.map((stop) => ({
- offset: start + stop.offset * (1 - start),
- opacity: (stop.opacity / peak) * edgeFade.cover,
- })),
+ ...(start > 0 ? [{ offset: 0, opacity: 1 }] : []),
+ ...Array.from({ length: edgeFade.samples + 1 }, (_, index) => {
+ const step = index / edgeFade.samples;
+ return { offset: start + step * (1 - start), opacity: (1 - step) ** edgeFade.curve };
+ }),
];
}
diff --git a/apps/mobile/src/ui/chat/header-chrome.tsx b/apps/mobile/src/ui/chat/header-chrome.tsx
index 5babf507a3..0f048aa721 100644
--- a/apps/mobile/src/ui/chat/header-chrome.tsx
+++ b/apps/mobile/src/ui/chat/header-chrome.tsx
@@ -101,7 +101,8 @@ export function EdgeFade({
const glass = useLiquidGlass();
const id = `fade-${useId().replace(/[^a-z0-9]/giu, '')}`;
const top = side === 'top';
- const stops = edgeFadeStops(height, cover);
+ const insets = useSafeAreaInsets();
+ const stops = edgeFadeStops(height, cover, top ? insets.top : insets.bottom);
return (
{
expect(edgeFadeStops(80, 0)).toBe(edgeFade.stops);
});
-it('stays nearly opaque under floating chrome and fades out past it', () => {
- const stops = edgeFadeStops(100, 76);
+it('reaches full opacity only at the screen edge and eases out over the whole fade', () => {
+ const stops = edgeFadeStops(140, 76);
expect(stops[0]).toEqual({ offset: 0, opacity: 1 });
- expect(stops[1]).toEqual({ offset: 0.76, opacity: edgeFade.cover });
expect(stops.at(-1)).toEqual({ offset: 1, opacity: 0 });
+ const chromeEdge = stops.find((stop) => stop.offset >= 76 / 140)!;
+ expect(chromeEdge.opacity).toBeLessThan(0.5);
for (let index = 1; index < stops.length; index += 1) {
expect(stops[index]!.offset).toBeGreaterThan(stops[index - 1]!.offset);
- expect(stops[index]!.opacity).toBeLessThanOrEqual(stops[index - 1]!.opacity);
+ expect(stops[index]!.opacity).toBeLessThan(stops[index - 1]!.opacity);
}
});
+
+it('holds the system bar strip fully opaque before the curve starts', () => {
+ const stops = edgeFadeStops(140, 76, 28);
+ expect(stops[0]).toEqual({ offset: 0, opacity: 1 });
+ expect(stops[1]).toEqual({ offset: 0.2, opacity: 1 });
+ expect(stops.at(-1)).toEqual({ offset: 1, opacity: 0 });
+});
diff --git a/changes/2425-soft-chat-fades.md b/changes/2425-soft-chat-fades.md
new file mode 100644
index 0000000000..0d19df9ef6
--- /dev/null
+++ b/changes/2425-soft-chat-fades.md
@@ -0,0 +1,11 @@
+---
+section: Changed
+---
+
+## en
+
+- The fade under the chat composer, and under the Android chat header, is softer. It reaches full opacity only near the screen edge instead of turning into a hard band at the composer's top. On Android the round Back, the Bot's name chip and the right button no longer touch ([#2425](https://github.com/BotHarness/DeepSeekBot/pull/2425)).
+
+## zh
+
+- 聊天输入框下方(以及 Android 聊天顶栏下方)的渐变更柔和了,只在靠近屏幕边缘时才完全不透明,不会在输入框上沿形成一条硬边。Android 顶栏的圆形返回按钮、Bot 名称 chip 和右侧按钮之间也留出了间距([#2425](https://github.com/BotHarness/DeepSeekBot/pull/2425))。