Skip to content

Commit 95386f5

Browse files
fix(site): improve agents page mobile view (#24508)
closes DES-22030 ## Summary Mobile view cleanup for the agents page — all changes are behind the `md:` breakpoint so desktop is unchanged. **Dropdowns:** Full-width on mobile with dynamic positioning via a `--mobile-dropdown-bottom` CSS custom property set by a `ResizeObserver` on the chat input box. Three position variants: `-bottom` (above chat input), `-top` (below header), `-top-below-header` (below sidebar header). Viewport branching uses a new `isBelowMdViewport()` helper (`< 768px`) so 640–767 px landscape phones pick the mobile branch instead of the desktop flyout. **Layout:** On the main agents page, mobile ordering is header → chat list → chat input using CSS `order` and `contents` on the content wrapper. The chat input aligns to the bottom of available space. The sidebar list uses a top/bottom fade mask on mobile to hint at scrollable content. **Header:** Settings, Analytics, sound, and notification icons consolidated into a single meatball menu dropdown on mobile. Sound/notification toggles use `e.preventDefault()` to keep the menu open for state feedback. Chime and notification state is lifted into `AgentCreatePage` and passed down, so the mobile meatball menu and the desktop `ChimeButton`/`WebPushButton` stay in sync. **Workspace pill:** Icon-only on mobile (`size-7` round button with `StatusIcon`), full pill on desktop. Tooltip hidden on mobile to prevent ghost tooltip after dropdown close. **Plus menu:** Workspace picker replaces the flyout with an inline sub-panel on mobile (back button + search list). Desktop flyout unchanged. `modal={false}` prevents double-tap when switching between dropdowns. **Model selector:** Truncated via `shrink` + `min-w-0` on mobile (flex-based, no fixed max-width), inline provider/context subtext per item, tooltip hidden on mobile. Added `open` / `onOpenChange` / `onTriggerTouchStart` props for external control. **Consistency:** All back/close buttons normalized to `ArrowLeftIcon`. Right panel, sidebar settings, header `mobileBack`, and workspace sub-panel all match the chat top bar pattern. **Misc polish:** Chat tree nodes use `select-none` + `-webkit-touch-callout:none` on coarse pointers to suppress the long-press selection/callout on mobile. <details> <summary>Files changed (18)</summary> - `site/src/index.css` — mobile dropdown CSS with 3 position variants - `site/src/utils/mobile.ts` — new `isBelowMdViewport()` helper (`<768px`) - `site/src/pages/AgentsPage/AgentChatPageView.tsx` — bottom padding `pb-3` - `site/src/pages/AgentsPage/AgentCreatePage.tsx` — lift chime + webpush state; pass handlers to header and buttons - `site/src/pages/AgentsPage/AgentsPageView.tsx` — `contents` wrapper + sidebar `border-b` - `site/src/pages/AgentsPage/components/AgentChatInput.tsx` — `ResizeObserver` composer ref, `plusMenuView` state, inline workspace picker, `modal={false}`, mobile branching via `isBelowMdViewport`, bg - `site/src/pages/AgentsPage/components/AgentCreateForm.tsx` — `order-last` + `items-end` on mobile - `site/src/pages/AgentsPage/components/AgentPageHeader.tsx` — meatball menu (controlled chime/webpush props), `ArrowLeftIcon`, `order-first`, padding, desktop/mobile branching via `matchMedia` - `site/src/pages/AgentsPage/components/AgentPageHeader.stories.tsx` — new Storybook coverage + `play` assertions that toggle state stays in sync across breakpoints - `site/src/pages/AgentsPage/components/ChimeButton.tsx` — optional controlled `enabled` / `onToggle` props - `site/src/pages/AgentsPage/components/WebPushButton.tsx` — optional controlled `webPush` / `onToggle` props - `site/src/pages/AgentsPage/components/ChatElements/CompactOrgSelector.tsx` — full-width dropdown class - `site/src/pages/AgentsPage/components/ChatElements/ModelSelector.tsx` — truncation, inline subtext, tooltip hidden on mobile, new props (`open`, `onOpenChange`, `onTriggerTouchStart`) - `site/src/pages/AgentsPage/components/ChatTopBar.tsx` — full-width dropdown class - `site/src/pages/AgentsPage/components/ContextUsageIndicator.tsx` — full-width dropdown class (mobile branch) - `site/src/pages/AgentsPage/components/Sidebar/AgentsSidebar.tsx` — `ArrowLeftIcon`, filter dropdown class, top/bottom fade mask on scroll area, `select-none` on tree nodes - `site/src/pages/AgentsPage/components/Sidebar/SidebarTabView.tsx` — `ArrowLeftIcon`, padding, back button placement - `site/src/pages/AgentsPage/components/WorkspacePill.tsx` — compact icon trigger, tooltip hidden on mobile, full-width dropdown class </details> > 🤖 Generated by Coder Agents --------- Co-authored-by: Jaayden Halko <jaayden@coder.com>
1 parent 537e35d commit 95386f5

18 files changed

Lines changed: 1068 additions & 403 deletions

site/src/index.css

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,81 @@
156156
}
157157
}
158158

159+
@layer components {
160+
/* Map each stripe variant to a color token so the
161+
pseudo-element rules can stay DRY. */
162+
.navbar-stripe-devel {
163+
--stripe-color: var(--content-warning);
164+
}
165+
166+
.navbar-stripe-rc {
167+
--stripe-color: var(--border-sky);
168+
}
169+
170+
/* Thin stripe bars at the top and bottom edges of the
171+
navbar. Using pseudo-elements keeps the stripes out of
172+
the content area so nav links stay readable. */
173+
.navbar-stripe-devel::before,
174+
.navbar-stripe-devel::after,
175+
.navbar-stripe-rc::before,
176+
.navbar-stripe-rc::after {
177+
content: "";
178+
position: absolute;
179+
left: 0;
180+
right: 0;
181+
height: 4px;
182+
background: repeating-linear-gradient(
183+
-45deg,
184+
transparent,
185+
transparent 4px,
186+
hsl(var(--stripe-color) / 0.5) 4px,
187+
hsl(var(--stripe-color) / 0.5) 8px
188+
);
189+
pointer-events: none;
190+
}
191+
192+
.navbar-stripe-devel::before,
193+
.navbar-stripe-rc::before {
194+
top: 0;
195+
}
196+
197+
.navbar-stripe-devel::after,
198+
.navbar-stripe-rc::after {
199+
bottom: 0;
200+
}
201+
202+
@media (max-width: 767px) {
203+
/*
204+
* Full-width mobile dropdowns. We set a --mobile-dropdown-bottom
205+
* custom property on the chat input container so the dropdown
206+
* position tracks the actual input box, not a hardcoded offset.
207+
*/
208+
[data-radix-popper-content-wrapper]:has(> .mobile-full-width-dropdown) {
209+
position: fixed !important;
210+
left: 1rem !important;
211+
width: calc(100vw - 2rem) !important;
212+
min-width: 0 !important;
213+
transform: none !important;
214+
bottom: var(--mobile-dropdown-bottom, 5rem) !important;
215+
top: auto !important;
216+
}
217+
[data-radix-popper-content-wrapper]:has(> .mobile-full-width-dropdown-top) {
218+
bottom: auto !important;
219+
top: var(--mobile-dropdown-top, 3.5rem) !important;
220+
}
221+
[data-radix-popper-content-wrapper]:has(
222+
> .mobile-full-width-dropdown-top-below-header
223+
) {
224+
bottom: auto !important;
225+
top: 5rem !important;
226+
}
227+
.mobile-full-width-dropdown {
228+
width: 100% !important;
229+
min-width: 0 !important;
230+
max-width: none !important;
231+
}
232+
}
233+
}
159234
@layer base {
160235
* {
161236
@apply border-border;

site/src/pages/AgentsPage/AgentChatPageView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ export const AgentChatPageView: FC<AgentChatPageViewProps> = ({
467467
/>
468468
</div>
469469
</ChatScrollContainer>
470-
<div className="shrink-0 overflow-y-auto px-4 pb-4 md:pb-0 [scrollbar-gutter:stable] [scrollbar-width:thin]">
470+
<div className="shrink-0 overflow-y-auto px-4 pb-3 md:pb-0 [scrollbar-gutter:stable] [scrollbar-width:thin]">
471471
<ChatPageInput
472472
organizationId={organizationId}
473473
store={store}
@@ -612,7 +612,7 @@ export const AgentChatPageLoadingView: FC<AgentChatPageLoadingViewProps> = ({
612612
</div>
613613
</div>
614614
</div>
615-
<div className="shrink-0 overflow-y-auto px-4 pb-4 md:pb-0 [scrollbar-gutter:stable] [scrollbar-width:thin]">
615+
<div className="shrink-0 overflow-y-auto px-4 pb-3 md:pb-0 [scrollbar-gutter:stable] [scrollbar-width:thin]">
616616
<AgentChatInput
617617
onSend={() => {}}
618618
initialValue=""

site/src/pages/AgentsPage/AgentCreatePage.tsx

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import type { FC } from "react";
1+
import { type FC, useState } from "react";
22
import { useMutation, useQuery, useQueryClient } from "react-query";
33
import { useNavigate } from "react-router";
4+
import { toast } from "sonner";
5+
import { getErrorMessage } from "#/api/errors";
46
import {
57
chatModelConfigs,
68
chatModels,
@@ -9,6 +11,7 @@ import {
911
} from "#/api/queries/chats";
1012
import { workspaces } from "#/api/queries/workspaces";
1113
import type * as TypesGen from "#/api/typesGenerated";
14+
import { useWebpushNotifications } from "#/contexts/useWebpushNotifications";
1215
import { useAuthenticated } from "#/hooks/useAuthenticated";
1316
import {
1417
AgentCreateForm,
@@ -17,6 +20,7 @@ import {
1720
import { AgentPageHeader } from "./components/AgentPageHeader";
1821
import { ChimeButton } from "./components/ChimeButton";
1922
import { WebPushButton } from "./components/WebPushButton";
23+
import { getChimeEnabled, setChimeEnabled } from "./utils/chime";
2024
import { getModelOptionsFromConfigs } from "./utils/modelOptions";
2125
import { buildAgentChatPath } from "./utils/navigation";
2226

@@ -33,6 +37,8 @@ const AgentCreatePage: FC = () => {
3337
const mcpServersQuery = useQuery(mcpServerConfigs());
3438
const workspacesQuery = useQuery(workspaces({ q: "owner:me", limit: 0 }));
3539
const createMutation = useMutation(createChat(queryClient));
40+
const webPush = useWebpushNotifications();
41+
const [chimeEnabled, setChimeEnabledState] = useState(getChimeEnabled);
3642

3743
const catalogModelOptions = getModelOptionsFromConfigs(
3844
chatModelConfigsQuery.data,
@@ -77,11 +83,35 @@ const AgentCreatePage: FC = () => {
7783
navigate(buildAgentChatPath({ chatId: createdChat.id }));
7884
};
7985

86+
const handleChimeToggle = () => {
87+
const next = !chimeEnabled;
88+
setChimeEnabledState(next);
89+
setChimeEnabled(next);
90+
};
91+
92+
const handleNotificationToggle = async () => {
93+
try {
94+
if (webPush.subscribed) {
95+
await webPush.unsubscribe();
96+
} else {
97+
await webPush.subscribe();
98+
}
99+
} catch (error) {
100+
const action = webPush.subscribed ? "disable" : "enable";
101+
toast.error(getErrorMessage(error, `Failed to ${action} notifications.`));
102+
}
103+
};
104+
80105
return (
81106
<>
82-
<AgentPageHeader>
83-
<ChimeButton />
84-
<WebPushButton />
107+
<AgentPageHeader
108+
chimeEnabled={chimeEnabled}
109+
onToggleChime={handleChimeToggle}
110+
webPush={webPush}
111+
onToggleNotifications={handleNotificationToggle}
112+
>
113+
<ChimeButton enabled={chimeEnabled} onToggle={handleChimeToggle} />
114+
<WebPushButton webPush={webPush} onToggle={handleNotificationToggle} />
85115
</AgentPageHeader>
86116
<AgentCreateForm
87117
onCreateChat={handleCreateChat}

site/src/pages/AgentsPage/AgentsPageView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
165165
? "hidden md:block shrink-0 h-[42dvh] min-h-[240px] border-b border-border-default"
166166
: isSettingsDetail || isAnalytics
167167
? "hidden md:block shrink-0"
168-
: "order-2 md:order-none flex-1 min-h-0 border-t border-border-default md:flex-none md:border-t-0",
168+
: "order-2 md:order-none flex-1 min-h-0 border-b border-border-default md:flex-none md:border-t-0 md:border-b-0",
169169
isSidebarCollapsed && "md:hidden",
170170
)}
171171
>
@@ -207,7 +207,7 @@ export const AgentsPageView: FC<AgentsPageViewProps> = ({
207207
!agentId &&
208208
!isSettingsDetail &&
209209
sidebarView.panel === "chats" &&
210-
"order-1 md:order-none flex-none md:flex-1",
210+
"contents md:flex md:flex-1 md:flex-col",
211211
)}
212212
>
213213
<Outlet context={outletContextValue} />

0 commit comments

Comments
 (0)