chore(web): mobile-responsive layout for dashboard and session views#634
chore(web): mobile-responsive layout for dashboard and session views#634ashish921998 merged 45 commits intomainfrom
Conversation
Add proper mobile breakpoints (768px, 480px) with structural layout changes: - Sidebar: auto-hide on mobile with hamburger toggle and overlay drawer - Dashboard hero: stack stats cards and controls vertically on mobile - Kanban board: stack columns vertically instead of horizontal scroll - Session cards: flexible height with 44px minimum touch targets - Session detail: responsive header metadata and full-width terminal - Global: phone-specific breakpoint (480px) for single-column layouts Desktop layout remains completely unchanged. Closes #633 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The collapsed sidebar early-return path was missing the backdrop overlay and close mechanism that the expanded branch had, making it impossible to dismiss the mobile drawer by tapping outside. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add web app manifest (manifest.ts) with dynamic project name, standalone display - Add service worker (sw.js) with network-first strategy and offline fallback page - Add PWA icons: apple-touch-icon (180x180), 192x192, 512x512 via shared renderer - Add viewport meta with viewport-fit=cover for safe-area-inset support - Add Apple Web App meta tags for iOS home screen installation - Add theme-color meta for browser chrome coloring - Extract shared icon-renderer.tsx to deduplicate icon generation logic - Add ServiceWorkerRegistrar client component for SW registration - Configure next.config.js with no-cache headers for service worker - Add dvh fallbacks for 100vh usage (body, kanban-board, Terminal, DirectTerminal) - Add safe-area-inset padding for mobile sidebar and dashboard main - Add iOS auto-zoom prevention (16px font on inputs) - Add -webkit-tap-highlight-color: transparent and text-size-adjust - Add PWA standalone mode body padding for safe areas Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace unsupported eslint-env with globals comment for flat config. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Increase .project-sidebar--mobile-open specificity to (0,2,0) so it overrides .project-sidebar.project-sidebar--collapsed transform - Make "Hide sidebar" and "Show project sidebar" buttons also call onMobileClose to dismiss the mobile drawer when toggling collapse Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
caches.match() can return undefined under mobile storage pressure. Fall back to a plain-text 503 response instead of passing undefined to event.respondWith(). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the lazy useState initializer that read window.matchMedia on first render (causing SSR/client hydration mismatch) with a plain false default; the existing useEffect already syncs the correct value after mount. Adds a full vitest test suite covering all six specified cases (SSR-safe default, match/no-match, number-to-query conversion, change-event reactivity, and listener cleanup on unmount). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace vertical kanban stacking on mobile (<=767px) with a single-open accordion ordered Respond > Merge > Review > Pending > Working. Auto-expands the most urgent non-empty section on load; empty sections render as header-only rows (no dashed placeholder). Desktop layout is unchanged. Added window.matchMedia stub to the Vitest setup file so Dashboard unit tests pass in jsdom. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix Critical 1: change onToggle to (level: AttentionLevel) => void and create stable handleAccordionToggle with useCallback, eliminating the inline arrow that defeated memo on every render - Fix Critical 2: remove grouped from useEffect deps so user accordion selection is not reset on every SSE update; seed only on isMobile change - Fix Important 3: move accordion CSS block inside @media (max-width: 767px) so it is scoped to mobile only, consistent with rest of file - Fix Important 4: add Safari 16+ compatibility comment on grid-template-rows transition - Fix Suggestion 5: replace ▶/▼ character swap with single ▶ rotated via CSS transform so the declared transition: transform actually animates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cards On mobile (<=767px), replace the 4 stat cards with a single-row MobileActionStrip showing non-zero urgency pills (respond/merge/review) with colored dots. Tapping a pill calls setExpandedLevel and scrolls to the accordion board via id="mobile-board". Hides stat cards, dashboard subtitle, and board-section-head on mobile to target ~92px hero height. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract onPillTap as handlePillTap useCallback with prefers-reduced-motion support - Replace role="navigation" with role="group" aria-label="Session priorities" on pill strip - Add role="status" to all-clear fallback so screen readers announce state changes - Add :focus-visible outline to .mobile-action-pill for keyboard accessibility - Add scroll-margin-top: 56px to .accordion-board to prevent sticky header occlusion Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On mobile (<=767px), session cards with attention level "respond" now show an inline quick-reply panel with the agent's last message summary (2-line clamp), Continue/Abort/Skip preset buttons, and an expandable textarea. Enter submits; all touch targets are >=44px. Hidden on desktop via CSS-only media query. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Quick-reply UI was invisible on mobile viewports 480–767px wide because its styles were scoped to @media (max-width: 480px). Moved all .quick-reply* rules into the @media (max-width: 767px) block so the component renders correctly across the full mobile range. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…order token Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…confirmations - Create Toast.tsx with ToastProvider + useToast hook (3s auto-dismiss, success/error/info variants, fade+slide animation) - Create BottomSheet.tsx replacing confirm() for kill actions (swipe-to-dismiss, keyboard Escape, session context display) - Wire ToastProvider + BottomSheet into Dashboard.tsx; replace confirm() calls; add showToast on merge/kill/restore success and error - Add toast and bottom-sheet CSS to globals.css (no new z-index vars needed — --z-modal/--z-toast already existed) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…mSheet - ToastProvider now clears its setTimeout in a useEffect cleanup to prevent memory leaks when the component unmounts while a toast is still pending. - BottomSheet adds a focus trap on the sheet element so keyboard users cannot Tab out of the dialog; focus is also set to the first focusable element (Cancel button) when the sheet opens. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Exposes connectionStatus ("connected" | "reconnecting" | "disconnected")
from useSessionEvents via onopen/onerror EventSource handlers, and renders
a fixed ConnectionBar at the top of the viewport that shows a pulsing amber
bar while reconnecting and a red 32px "Offline · tap to retry" bar when
disconnected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Code reviewFound 1 issue:
agent-orchestrator/packages/web/src/components/SessionDetail.tsx Lines 415 to 425 in 740933e 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
There are 3 total unresolved issues (including 1 from previous review).
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Cache deduplication defeated by inconsistent argument passing
- Removed redundant resolveDashboardProjectFilter calls in generateMetadata functions so both generateMetadata and page components pass the same raw searchParams.project value to getDashboardPageData, ensuring cache key consistency.
- ✅ Fixed: Session detail page lacks mobile bottom nav padding
- Added padding-bottom: calc(84px + env(safe-area-inset-bottom, 0px)) to .session-detail-page .mx-auto in the mobile breakpoint to match the dashboard-main padding pattern and prevent content from being hidden behind the fixed bottom nav.
Or push these changes by commenting:
@cursor push 6e4cda3a39
Preview (6e4cda3a39)
diff --git a/packages/web/src/app/globals.css b/packages/web/src/app/globals.css
--- a/packages/web/src/app/globals.css
+++ b/packages/web/src/app/globals.css
@@ -970,7 +970,8 @@
}
@keyframes session-status-dot-pulse {
- 0%, 100% {
+ 0%,
+ 100% {
transform: scale(1);
opacity: 0.88;
}
@@ -981,7 +982,8 @@
}
@keyframes session-status-active-breathe {
- 0%, 100% {
+ 0%,
+ 100% {
box-shadow:
inset 0 1px 0 color-mix(in srgb, white 16%, transparent),
0 0 0 0 color-mix(in srgb, var(--color-status-working) 0%, transparent);
@@ -994,7 +996,8 @@
}
@keyframes session-status-ready-breathe {
- 0%, 100% {
+ 0%,
+ 100% {
box-shadow:
inset 0 1px 0 color-mix(in srgb, white 16%, transparent),
0 0 0 0 color-mix(in srgb, var(--color-status-ready) 0%, transparent);
@@ -1007,7 +1010,8 @@
}
@keyframes session-status-waiting-breathe {
- 0%, 100% {
+ 0%,
+ 100% {
box-shadow:
inset 0 1px 0 color-mix(in srgb, white 16%, transparent),
0 0 0 0 color-mix(in srgb, var(--color-status-attention) 0%, transparent);
@@ -1936,7 +1940,6 @@
/* Card glow effects removed for Linear-clean look */
-
@media (max-width: 960px) {
.dashboard-hero__content {
padding: 12px 14px;
@@ -2173,8 +2176,7 @@
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
padding: 8px max(12px, env(safe-area-inset-right, 0px))
- calc(8px + env(safe-area-inset-bottom, 0px))
- max(12px, env(safe-area-inset-left, 0px));
+ calc(8px + env(safe-area-inset-bottom, 0px)) max(12px, env(safe-area-inset-left, 0px));
}
.mobile-bottom-nav__item {
@@ -2407,6 +2409,7 @@
.session-detail-page .mx-auto {
padding-left: 12px;
padding-right: 12px;
+ padding-bottom: calc(84px + env(safe-area-inset-bottom, 0px));
}
.session-detail-page {
@@ -2546,7 +2549,9 @@
}
/* -- iOS auto-zoom prevention on input focus -- */
- input, textarea, select {
+ input,
+ textarea,
+ select {
font-size: 16px;
}
@@ -2876,7 +2881,10 @@
font-family: var(--font-sans);
font-size: 12px;
cursor: pointer;
- transition: border-color 0.12s ease, color 0.12s ease, background 0.12s ease;
+ transition:
+ border-color 0.12s ease,
+ color 0.12s ease,
+ background 0.12s ease;
}
.quick-reply__preset-btn:hover {
@@ -2896,7 +2904,9 @@
font-size: 13px;
resize: none;
overflow: hidden;
- transition: height 0.15s ease, border-color 0.12s ease;
+ transition:
+ height 0.15s ease,
+ border-color 0.12s ease;
box-sizing: border-box;
width: 100%;
}
@@ -3301,7 +3311,9 @@
z-index: var(--z-connection-bar, 500);
height: 0;
overflow: hidden;
- transition: height 0.2s ease, opacity 0.3s ease;
+ transition:
+ height 0.2s ease,
+ opacity 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
@@ -3327,6 +3339,11 @@
}
@keyframes connection-pulse {
- 0%, 100% { opacity: 1; }
- 50% { opacity: 0.4; }
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.4;
+ }
}
diff --git a/packages/web/src/app/page.tsx b/packages/web/src/app/page.tsx
--- a/packages/web/src/app/page.tsx
+++ b/packages/web/src/app/page.tsx
@@ -8,8 +8,7 @@
searchParams: Promise<{ project?: string }>;
}): Promise<Metadata> {
const searchParams = await props.searchParams;
- const projectFilter = resolveDashboardProjectFilter(searchParams.project);
- const pageData = await getDashboardPageData(projectFilter);
+ const pageData = await getDashboardPageData(searchParams.project);
return { title: { absolute: `ao | ${pageData.projectName}` } };
}
diff --git a/packages/web/src/app/prs/page.tsx b/packages/web/src/app/prs/page.tsx
--- a/packages/web/src/app/prs/page.tsx
+++ b/packages/web/src/app/prs/page.tsx
@@ -8,8 +8,7 @@
searchParams: Promise<{ project?: string }>;
}): Promise<Metadata> {
const searchParams = await props.searchParams;
- const projectFilter = resolveDashboardProjectFilter(searchParams.project);
- const pageData = await getDashboardPageData(projectFilter);
+ const pageData = await getDashboardPageData(searchParams.project);
return { title: { absolute: `ao | ${pageData.projectName} PRs` } };
}This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.
harsh-batheja
left a comment
There was a problem hiding this comment.
Code Review — Post-fix issues & remaining unresolved items
New issues introduced after previous review (5)
handleSendthrow creates inconsistent error UX on action buttons (Medium)handleMerge/handleRestorelack try/catch for network failures (Medium)projectOrchestratorIdreset effect creates re-fetch burst loop (Medium)- Quick-reply desktop styling uses mobile-sized 44px touch targets (Low)
- Desktop PRs link flashes on mobile before
useMediaQuerykicks in (Low)
Still unresolved from previous review (2)
- Non-orchestrator session pages still poll
/api/sessionsevery 5s (Medium) - User accordion collapse overridden by SSE updates when
mobileFilter === "all"(Medium)
See inline comments for details.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
chore(web): mobile-responsive layout for dashboard and session views


Summary
Adds proper mobile-responsive layouts to the dashboard and session views after the recent UI/UX redesign (#528).
Desktop layout remains completely unchanged.
Closes #633
Test plan
🤖 Generated with Claude Code