From af2af115bd1f835e1e17db3665d9db50c99845ab Mon Sep 17 00:00:00 2001 From: Ashish Huddar <73213873+ashish921998@users.noreply.github.com> Date: Sat, 11 Apr 2026 16:30:03 +0530 Subject: [PATCH] style(design): design review fixes + fresh Warm Terminal design system (#927) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * style(design): FINDING-001 — add prefers-reduced-motion support All animations and transitions are disabled when the user's system requests reduced motion, per DESIGN.md accessibility requirements. Co-Authored-By: Claude Opus 4.6 (1M context) * style(design): FINDING-003 — remove concurrent breathe animations Status pills had two animations: a breathe animation on the pill and a dot-pulse on the child dot. DESIGN.md says "one animation per element, one purpose" and "keep dot pulse, remove border heartbeat." Removed all three breathe keyframes, kept dot-pulse only. Co-Authored-By: Claude Opus 4.6 (1M context) * style(design): FINDING-004 — fix dashboard title weight and tracking DESIGN.md specifies display headings at weight 680 and letter-spacing -0.035em. The dashboard title was using 600 / -0.05em. Co-Authored-By: Claude Opus 4.6 (1M context) * style(design): FINDING-005 — fix detail-card text to blue-tinted graphite Detail cards overrode text-secondary and text-tertiary with neutral grays (#9898a0, #5c5c66). DESIGN.md specifies blue-tinted graphite palette (#a5afc4, #6f7c94) for dark mode text. Co-Authored-By: Claude Opus 4.6 (1M context) * style(design): FINDING-008 — add text-wrap: balance on headings Dashboard title and kanban column titles now use text-wrap: balance for more even line breaks on narrow viewports. Co-Authored-By: Claude Opus 4.6 (1M context) * style(design): FINDING-006/009 — fix section label semantics and spacing Changed "Attention Board" from

to
since it's styled as a 12px uppercase label, not a heading. Also fixed letter-spacing from 0.16em to 0.06em per DESIGN.md UI/Labels spec. Co-Authored-By: Claude Opus 4.6 (1M context) * style(design): FINDING-007 — contextual empty state messages Empty kanban columns now show context-specific messages instead of generic "No sessions" text. Each column's empty state reflects its purpose: "No agents need your input" (Respond), "No code waiting for review" (Review), etc. Co-Authored-By: Claude Opus 4.6 (1M context) * docs(design): fresh design system — Warm Terminal Complete redesign from Industrial Precision (blue-tinted) to Warm Terminal (brown-tinted). Key changes: - Warm charcoal surfaces (#121110, #1a1918, #222120) replace blue-gray - Cream text (#f0ece8) replaces blue-white (#eef3ff) - Warm periwinkle accent (#8b9cf7) replaces cool blue (#5B7EF8) - Berkeley Mono for display headlines (mono cohesion) - Added: Accessibility section (44px touch targets, WCAG AA, focus-visible) - Added: Component anatomy (button states, card structure, input fields) - Added: Light mode design rationale (warm parchment, not clinical white) Co-Authored-By: Claude Opus 4.6 (1M context) * docs(design): swap Berkeley Mono for JetBrains Mono (free) Berkeley Mono is a paid font ($75). JetBrains Mono is free, open source, already loaded in the project, and the mono-for-headlines concept works the same way with it. Co-Authored-By: Claude Opus 4.6 (1M context) * docs(design): fix light mode contrast failures Light mode text-tertiary #a8a29e failed WCAG AA at 2.5:1 on white. Darkened to #736e6b (5.0:1). Light mode accent #6b73c4 was borderline at 4.3:1, darkened to #5c64b5 (5.3:1). All pairs now pass AA. Added verified contrast ratios for both modes to accessibility section. Co-Authored-By: Claude Opus 4.6 (1M context) * chore: gitignore .gstack/ directory Co-Authored-By: Claude Opus 4.6 (1M context) * docs(design): add design audit report and screenshots Design review audit report with before/after screenshots for all dashboard pages (kanban, session detail, PRs) across desktop, tablet, and mobile viewports in both light and dark mode. Co-Authored-By: Claude Opus 4.6 (1M context) * fix(design): address PR review comments - Fix mobile test expecting removed "No sessions" text. The merge zone emptyMessage is now "Nothing cleared to land yet." (Bugbot comment #1) - Remove no-op .dark .detail-card override that duplicated global dark values after FINDING-005 fix aligned them (Bugbot comment #2) Co-Authored-By: Claude Opus 4.6 (1M context) * fix: gitignore .gstack-report/ and remove from tracking The .gstack-report/ directory contains local audit artifacts with filesystem paths. Should not be tracked in the repository. Co-Authored-By: Claude Opus 4.6 (1M context) * fix(design): align dashboard title CSS to new DESIGN.md spec Dashboard title was using old Geist Sans values (weight 680, -0.035em). New spec is JetBrains Mono, weight 500, letter-spacing -0.02em. Added font-family: var(--font-mono) to match. Co-Authored-By: Claude Opus 4.6 (1M context) * fix: use native h2 element for Attention Board section heading Replace ARIA role="heading" div with semantic h2 per ARIA first rule — native elements are preferred over ARIA roles for actual headings. Co-Authored-By: Claude Sonnet 4.6 * fix: pre-landing review fixes — a11y, dead code, test coverage - Add aria-controls + id to accordion button/body pair in AttentionZone - Wrap empty-state messages in aria-live="polite" regions for AT announcements - Remove dead message prop and isDefault from EmptyState (Skeleton.tsx) - Add parameterized test covering all 6 zone-specific empty messages Co-Authored-By: Claude Sonnet 4.6 * fix: revert aria-live on empty states — causes false AT announcements Codex review identified that role="status" aria-live on static empty-state text causes burst announcements on page load (all empty columns fire) and announces in collapsed mobile sections that aren't visible. Empty states are static text, not dynamic transitions. The aria-controls fix is kept. Co-Authored-By: Claude Sonnet 4.6 * chore: bump version and changelog (v0.0.1.0) Co-Authored-By: Claude Sonnet 4.6 * chore: remove .gstack-report/ from .gitignore * chore: remove VERSION and CHANGELOG (not used in this project) * style(design): warm terminal color migration + inline style removal Migrate all CSS tokens from cool blue-tinted graphite to warm brown-tinted terminal aesthetic per DESIGN.md spec. Replace inline style color mappings in ActivityDot, AttentionZone, Dashboard, ProjectSidebar, and SessionCard with data-attribute CSS selectors. Fix duplicate className bug on SessionCard done-title element. Co-Authored-By: Claude Opus 4.6 * fix: pre-landing review fixes — activity dot fallback + review stat color Add base CSS fallback for activity-dot, activity-pill, and activity-pill__text so null/unknown activity states render visibly (gray) instead of invisible. Fix review stat card to use accent-orange (matching kanban/sidebar/mobile review indicators) instead of cyan. Co-Authored-By: Claude Opus 4.6 * chore: checkpoint current design branch state * design changes * feat(web): redesign session detail page — compact PR card, identity strip, layout reorder - Redesign SessionTopStrip with simplified breadcrumbs, action buttons (Message/Kill) - Replace stacked PR card with compact inline layout: title row + blocker/CI chips + collapsible comments - Move PR card above terminal for better information hierarchy - Replace vertical IssuesList with inline buildBlockerChips helper - Add ~200 lines of new CSS classes for compact PR card design system - Add changedFiles field to DashboardPR type Co-Authored-By: Claude Sonnet 4.6 * feat(web): design system tokens, sidebar redesign, and component primitives - Align all color tokens (status, bg, border, text) across three HTML mockups - Rewrite ProjectSidebar to match finalized.html: rotation chevron, session status text, border-bottom project separators, 224px width - Add packages/web/DESIGN.md: agent-readable reference for tokens, typography, component patterns, anti-patterns - Add Badge.tsx: generic badge/chip/pill primitive with status/outline/default variants - Add Button.tsx: ghost/primary/danger button primitive - Update CLAUDE.md to reference DESIGN.md as required pre-read for web UI work Co-Authored-By: Claude Sonnet 4.6 * style(design): FINDING-001 — card border-radius 0 → 6px to match mockup * style(design): FINDING-002 — column border-radius 0 → 7px, border subtle to match mockup * style(design): FINDING-003 — column header mono font, 500 weight, muted color to match mockup * style(design): FINDING-004/005/006 — fix accent-blue/yellow/purple tokens to match mockup * style(design): FINDING-007 — add --color-bg-card token (light #fff, dark #1c1b19) * Refine dashboard design system and remove fixture flow * Fix respond status colors in dashboard indicators * Align tests with updated dashboard and metadata behavior * fix(core): register notifier aliases consistently * chore(web): drop uncovered showcase routes * Add desktop PullRequestsPage coverage tests * Remove generated coverage artifact * Consolidate web design guidance into the root design system * Fix working and ready status color tokens * Fix sidebar collapse and inline kill confirmation * fix(qa): ISSUE-001 - show all mobile filter chips * Restore full title contrast in session cards * Fix review feedback in dashboard state styling * style: implement mobile responsive designs (feed, terminal-first, dense PRs) Dashboard: replace accordion with urgency-sorted priority feed, horizontal scroll filter pills. Session Detail: terminal-first layout with floating header, status pill, PR bottom sheet. PRs: dense rows with CI dots, grouped sections, muted merged/closed rows. Update tests to match new mobile layouts. Co-Authored-By: Claude Opus 4.6 * Fix mobile terminal padding with PR sheet layout * Polish mobile feed and session detail styling * Align mobile dashboard layouts with gstack designs * Fix mobile terminal actions and PR review labels --------- Co-authored-by: Claude Opus 4.6 (1M context) --- .gitignore | 1 + CLAUDE.md | 3 +- DESIGN.md | 290 +- packages/core/package.json | 40 + .../src/__tests__/lifecycle-manager.test.ts | 68 +- packages/core/src/lifecycle-manager.ts | 40 +- .../web/src/__tests__/components.test.tsx | 33 +- packages/web/src/__tests__/setup.ts | 9 +- packages/web/src/app/globals.css | 4248 +++++++++++++---- packages/web/src/app/layout.test.ts | 37 + packages/web/src/app/layout.tsx | 4 +- packages/web/src/app/manifest.test.ts | 25 + packages/web/src/app/manifest.ts | 4 +- packages/web/src/app/sessions/[id]/page.tsx | 22 +- packages/web/src/components/ActivityDot.tsx | 46 +- packages/web/src/components/AttentionZone.tsx | 41 +- packages/web/src/components/BottomSheet.tsx | 128 +- packages/web/src/components/Dashboard.tsx | 1289 ++--- .../web/src/components/DirectTerminal.tsx | 282 +- packages/web/src/components/PRStatus.tsx | 97 +- .../web/src/components/ProjectSidebar.tsx | 489 +- .../web/src/components/PullRequestsPage.tsx | 335 +- packages/web/src/components/SessionCard.tsx | 527 +- packages/web/src/components/SessionDetail.tsx | 810 ++-- packages/web/src/components/Skeleton.tsx | 120 +- packages/web/src/components/ThemeToggle.tsx | 15 +- .../__tests__/Dashboard.emptyState.test.tsx | 36 +- .../__tests__/Dashboard.mobile.test.tsx | 180 +- .../Dashboard.projectOverview.test.tsx | 11 +- .../__tests__/DirectTerminal.render.test.tsx | 17 + .../__tests__/PRStatus.coverage.test.tsx | 20 +- .../__tests__/ProjectSidebar.test.tsx | 227 +- .../__tests__/PullRequestsPage.test.tsx | 56 + .../__tests__/SessionDetail.desktop.test.tsx | 189 + .../__tests__/SessionDetail.mobile.test.tsx | 68 +- packages/web/src/hooks/useSessionEvents.ts | 9 +- packages/web/src/lib/__tests__/types.test.ts | 16 + packages/web/src/lib/types.ts | 1 + 38 files changed, 6927 insertions(+), 2906 deletions(-) create mode 100644 packages/web/src/app/layout.test.ts create mode 100644 packages/web/src/app/manifest.test.ts create mode 100644 packages/web/src/components/__tests__/SessionDetail.desktop.test.tsx diff --git a/.gitignore b/.gitignore index f3bc5833c..93f75b637 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ node_modules/ dist/ .pnpm-store/ .next/ +.next-dev/ *.tsbuildinfo coverage/ *.patch diff --git a/CLAUDE.md b/CLAUDE.md index 20e63a70f..ce2cc6c6f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -201,7 +201,8 @@ Hash = SHA-256 of config directory (first 12 chars). Prevents collision across m | `packages/web/src/components/SessionCard.tsx` | Kanban session card | | `packages/web/src/hooks/useSessionEvents.ts` | SSE consumer hook | | `packages/web/src/lib/types.ts` | Dashboard types | -| `packages/web/src/app/globals.css` | Design tokens and base styles | +| `packages/web/src/app/globals.css` | Design tokens and base styles (full token definitions) | +| `DESIGN.md` | **Design system reference** — design principles, token mapping, component patterns, anti-patterns (read this before writing any web UI) | | `agent-orchestrator.yaml` | Project-level config (user-created) | | `eslint.config.js` | ESLint flat config | | `tsconfig.base.json` | Shared TypeScript base config | diff --git a/DESIGN.md b/DESIGN.md index aa2786272..b606a2c89 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -3,17 +3,17 @@ ## Product Context - **What this is:** A web-based dashboard for managing fleets of parallel AI coding agents. Each agent gets its own git worktree, branch, and PR. The dashboard is the operator's single pane of glass. - **Who it's for:** Developers running 10-30+ AI coding agents in parallel. From solo devs to engineering teams. -- **Space/industry:** AI agent orchestration. Competitors: Conductor.build, T3 Code, OpenAI Codex app, Emdash. All are native Mac apps. Agent Orchestrator is the web-based alternative. +- **Space/industry:** AI agent orchestration. Competitors: Conductor.build, T3 Code, OpenAI Codex app. All are native Mac apps with cool blue-gray dark mode. Agent Orchestrator is the web-based alternative. - **Project type:** Web app (Next.js 15, React 19, Tailwind v4). Kanban board with 6 attention-priority columns. ## Aesthetic Direction -- **Direction:** Industrial Precision -- **Decoration level:** Intentional — subtle depth through surface hierarchy, ambient glow on active states, gradient surfaces in dark mode. No decorative blobs, no gratuitous gradients. -- **Mood:** Trading terminal meets control room. Dense, scannable, utilitarian, with just enough warmth that developers want to live in it for 10 hours. "I'm running an operation" not "I'm organizing tasks." -- **Reference sites:** Conductor.build, t3.codes, openai.com/codex, emdash.dev +- **Direction:** Warm Terminal +- **Decoration level:** Intentional — subtle surface depth through warm gradients, inset highlights that catch light like brushed aluminum, ambient glow on active states. No decorative blobs, no gratuitous effects. +- **Mood:** High-end audio gear meets flight deck. Dense, scannable, utilitarian, with enough warmth that developers want to live in it for 10 hours. Every competitor is cold blue-gray. This is the warm one. +- **Reference sites:** Conductor.build (layout baseline), linear.app (density standard), t3.codes (terminal aesthetic) ## Typography -- **Display/Hero:** Geist Sans, weight 680, letter-spacing -0.035em — same font as body but differentiated through weight and tracking. Tighter and heavier creates display hierarchy without a font swap. No cognitive gear-shifts on a scan-heavy dashboard. +- **Display/Hero:** JetBrains Mono, weight 500, letter-spacing -0.02em — monospace for headlines. In a dashboard where 40% of visible text is already monospace (agent output, branch names, commit hashes), leaning into mono for display creates a unified typographic voice instead of two competing voices. - **Body:** Geist Sans, weight 400, letter-spacing -0.011em — purpose-built for dense interfaces at 13px. Better digit alignment than IBM Plex Sans, designed for exactly this density level. - **UI/Labels:** Geist Sans, weight 600, letter-spacing 0.06em, uppercase, 10-11px — column headers, section labels, status indicators. - **Data/Tables:** JetBrains Mono, weight 400, 11-13px, tabular-nums — agent IDs, branch names, timestamps, commit hashes, diff stats, PR numbers. @@ -28,62 +28,66 @@ - display: clamp(22px, 2.8vw, 32px) (hero headings) ## Color -- **Approach:** Restrained with signal accents. Color is a priority channel, not decoration. -- **Accent (cool):** #5B7EF8 — interactive elements, links, focus rings. Used sparingly. -- **Accent hover:** #7a96ff -- **Accent tint:** rgba(91, 126, 248, 0.12) -- **Attention (warm):** #f1be64 — states requiring human input. Amber is universally "needs attention" without the panic of red. +- **Approach:** Restrained with signal accents. Color is a priority channel, not decoration. Warm tones throughout. +- **Accent (interactive):** #8b9cf7 — warm periwinkle. Links, focus rings, active states. Blue = clickable is muscle memory. This warm-leaning blue fits the palette without colliding with status colors. +- **Accent hover:** #a3b1fa +- **Accent tint:** rgba(139, 156, 247, 0.12) +- **Attention (warm):** #e2a336 — states requiring human input. Amber is universally "needs attention" without the panic of red. ### Surfaces (Dark Mode) -| Token | Value | Usage | -|-------|-------|-------| -| bg-base | #0a0d12 | Page background | -| bg-surface | #11161d | Card/column backgrounds | -| bg-elevated | #171d26 | Modals, popovers, hover states | -| bg-elevated-hover | #1c2430 | Hover on elevated surfaces | -| bg-subtle | rgba(177, 206, 255, 0.05) | Subtle tints, pill backgrounds | +| Token | Value | Usage | Rationale | +|-------|-------|-------|-----------| +| bg-base | #121110 | Page background | Brown-tinted black. Warmer than neutral #111 or blue-tinted #0a0d12. Sets the warm foundation. | +| bg-surface | #1a1918 | Card/column backgrounds | One stop lighter, same warm undertone. Surface hierarchy through subtle warmth, not just lightness. | +| bg-elevated | #222120 | Modals, popovers, hover states | Two stops up. Warm enough to feel distinct from surface without being muddy. | +| bg-elevated-hover | #2a2928 | Hover on elevated surfaces | Subtle lift on interaction. | +| bg-subtle | rgba(255, 240, 220, 0.04) | Subtle tints, pill backgrounds | Warm-tinted transparency. Reads as "highlighted" without introducing a new color. | ### Surfaces (Light Mode) -| Token | Value | Usage | -|-------|-------|-------| -| bg-base | #f5f5f7 | Page background | -| bg-surface | #ffffff | Card/column backgrounds | -| bg-elevated | #ffffff | Modals, popovers | -| bg-elevated-hover | #f7f7f8 | Hover states | -| bg-subtle | #f0f0f2 | Subtle tints | +| Token | Value | Usage | Rationale | +|-------|-------|-------|-----------| +| bg-base | #f5f3f0 | Page background | Warm parchment, not clinical white or cool gray. Matches the warm dark mode without being beige. | +| bg-surface | #ffffff | Card/column backgrounds | True white for cards creates contrast against the warm base. Cards "float" on warm paper. | +| bg-elevated | #ffffff | Modals, popovers | Same as surface. Light mode doesn't need as many elevation steps because shadows do the work. | +| bg-elevated-hover | #f7f5f2 | Hover states | Warm tint on hover, matching the base temperature. | +| bg-subtle | rgba(120, 100, 80, 0.05) | Subtle tints | Brown-tinted transparency for warm highlighting. | + +**Light mode strategy:** Warm parchment base (#f5f3f0) with white cards. The same brown undertone that makes dark mode warm also makes light mode feel like quality paper, not sterile lab equipment. Accent darkened in light mode (#5c64b5) to maintain 5.3:1 contrast on white. Status colors shifted darker (green #16a34a, amber #b8860b, red #dc2626, cyan #0891b2) to maintain contrast on light backgrounds. Drop shadows replace inset highlights for surface hierarchy. ### Text (Dark Mode) | Token | Value | Usage | |-------|-------|-------| -| text-primary | #eef3ff | Headings, card titles, body. Blue-white, not pure white. | -| text-secondary | #a5afc4 | Descriptions, metadata. Readable in dense layouts. | -| text-tertiary | #6f7c94 | Timestamps, placeholders, disabled states. | +| text-primary | #f0ece8 | Headings, card titles, body. Cream, not pure white or blue-white. Warm and easy on the eyes at 3am. | +| text-secondary | #a8a29e | Descriptions, metadata. Stone-toned, not neutral gray. Readable in dense layouts. | +| text-tertiary | #78716c | Timestamps, placeholders, disabled states. Warm tertiary that recedes without disappearing. | ### Text (Light Mode) | Token | Value | Usage | |-------|-------|-------| -| text-primary | #1b1b1f | Headings, card titles, body. | -| text-secondary | #5e5e66 | Descriptions, metadata. | -| text-tertiary | #8e8e96 | Timestamps, placeholders. | +| text-primary | #1c1917 | Headings, card titles, body. Warm near-black, not pure black. | +| text-secondary | #57534e | Descriptions, metadata. Stone-500. | +| text-tertiary | #736e6b | Timestamps, placeholders. Darkened from #a8a29e to pass WCAG AA (5.0:1 on white, 4.5:1 on base). | ### Borders (Dark Mode) | Token | Value | Usage | |-------|-------|-------| -| border-subtle | rgba(160, 190, 255, 0.08) | Dividers, section separators | -| border-default | rgba(160, 190, 255, 0.14) | Card edges, input borders | -| border-strong | rgba(185, 214, 255, 0.24) | Hover states, focus indicators | +| border-subtle | rgba(255, 240, 220, 0.06) | Dividers, section separators. Warm-tinted transparency. | +| border-default | rgba(255, 240, 220, 0.10) | Card edges, input borders. | +| border-strong | rgba(255, 240, 220, 0.18) | Hover states, focus indicators. | ### Status Colors -| Status | Dark Mode | Light Mode | Usage | -|--------|-----------|------------|-------| -| Working | #22c55e | #16a34a | Agent actively coding. Green dot with pulse ring animation. | -| Ready | #5B7EF8 | #5e6ad2 | Queued, awaiting start or CI pending. | -| Respond | #f1be64 | #ca8a04 | Needs human input. Amber = attention without panic. | -| Review | #06b6d4 | #0891b2 | Code ready for review. Cyan = "look when ready." | -| Error | #ef4444 | #dc2626 | CI failed, agent crashed. Red = broken. | -| Done | #3a4252 | #d0d7de | Completed. Fades to secondary text. Done items recede. | +| Status | Dark Mode | Light Mode | CSS Token | Usage | +|--------|-----------|------------|-----------|-------| +| Working | #22c55e | #16a34a | `--status-working` | Agent actively coding. Green dot with pulse ring animation. | +| Ready | #8b9cf7 | #6b73c4 | `--status-ready` | Queued, awaiting start or CI pending. | +| Respond | #e2a336 | #b8860b | `--status-respond` | Needs human input. Amber = attention without panic. **NOT red** — "respond" is a normal workflow state. | +| Review | #06b6d4 | #0891b2 | `--status-review` | Code ready for review. Cyan = "look when ready." | +| Error | #ef4444 | #dc2626 | `--status-error` | CI failed, agent crashed. Red = broken. Distinct from Respond. | +| Done | #57534e | #d6d3d1 | `--status-done` | Completed. Fades to stone. Done items recede. | -- **Dark mode strategy:** Blue-tinted graphite palette (not neutral gray). Reduce font weight by one step in dark mode (semibold becomes 500, bold becomes 600). Inset highlights on elevated surfaces: `inset 0 1px 0 rgba(255,255,255,0.04)`. Subtle radial gradients on body for ambient depth. +**Critical:** `--status-respond` and `--status-error` are separate tokens with different semantic meanings. Respond = human decision needed (amber). Error = something broke (red). Never conflate them. + +- **Dark mode strategy:** Warm charcoal palette (brown-tinted, not neutral or blue-tinted gray). Reduce font weight by one step in dark mode (semibold becomes 500, bold becomes 600). Inset highlights on elevated surfaces: `inset 0 1px 0 rgba(255,255,255,0.03)`. Subtle radial gradients on body for ambient depth. ## Spacing - **Base unit:** 4px @@ -96,12 +100,9 @@ - **Mobile column order:** Respond > Review > Pending > Working (urgency-first) - **Max content width:** 1280px for settings/detail pages - **Border radius:** - - base: 2px (cards, buttons, inputs — consistent, sharp, intentional) - - sm: 4px (tooltips, small transient elements) - - md: 6px (dropdowns, floating interactive elements) - - lg: 8px (modals, large floating overlays) - - full: 9999px (pills, badges, count indicators) -- **Card inset highlight:** `inset 0 1px 0 rgba(255,255,255,0.04)` in dark mode + - 0px everywhere. No rounding on cards, buttons, inputs, modals, dropdowns. Hard edges are the identity. The only exception is status dots (circles by nature) and avatar images. + - full: 9999px (status dots, avatar circles only) +- **Card inset highlight:** `inset 0 1px 0 rgba(255,255,255,0.03)` in dark mode - **Status accent:** 2px solid left border on session cards, colored by status ## Motion @@ -126,6 +127,167 @@ - All animations must respect `prefers-reduced-motion: reduce` - Use `contain: layout style paint` on session cards for performance with 30+ cards +## Accessibility +- **Touch targets:** Minimum 44x44px on all interactive elements (buttons, links, toggles). Icon buttons that render smaller visually must have padding to meet 44px minimum hit area. +- **Contrast ratios (WCAG AA):** + - Body text (13px): 4.5:1 minimum against surface backgrounds + - Large text (18px+ or 14px bold): 3:1 minimum + - UI components (borders, icons): 3:1 minimum against adjacent colors + - Dark: text-primary #f0ece8 on bg-surface #1a1918: 14.9:1 ✓ + - Dark: text-secondary #a8a29e on bg-surface #1a1918: 7.0:1 ✓ + - Dark: text-tertiary #78716c on bg-surface #1a1918: 3.7:1 ✓ (labels only, not body text) + - Dark: accent #8b9cf7 on bg-surface #1a1918: 6.9:1 ✓ + - Light: text-primary #1c1917 on bg-surface #ffffff: 17.5:1 ✓ + - Light: text-secondary #57534e on bg-surface #ffffff: 7.6:1 ✓ + - Light: text-tertiary #736e6b on bg-surface #ffffff: 5.0:1 ✓ + - Light: accent #5c64b5 on bg-surface #ffffff: 5.3:1 ✓ +- **Focus indicators:** `outline: 2px solid var(--accent); outline-offset: 2px` on `:focus-visible`. Never `outline: none` without a visible replacement. +- **Reduced motion:** `@media (prefers-reduced-motion: reduce)` disables all animations and transitions globally. Non-negotiable. +- **Color independence:** Never encode meaning with color alone. Always pair colored dots with text labels. Status pills include both dot and text. +- **Keyboard navigation:** All interactive elements reachable via Tab. Logical tab order. Escape closes modals/popovers. Arrow keys navigate within lists. +- **Screen reader:** ARIA labels on all icon-only buttons. `role="heading"` with `aria-level` on non-heading elements styled as headings. Status changes announced via `aria-live` regions. + +## Web Implementation Rules +- **Single source of truth:** This file is the authoritative design spec for the repo, including `packages/web/`. Do not create a second package-level `DESIGN.md`. +- **Tokens over raw values:** In React/Tailwind code, use CSS variables from `packages/web/src/app/globals.css` instead of hardcoded hex, rgba, or `dark:` overrides. +- **No inline styles:** Avoid `style={{ ... }}` for theme values. Use Tailwind utilities with `var(--token)` or add a named class in `globals.css`. +- **No external UI kits:** Do not introduce Radix, shadcn, Headless UI, or similar component libraries for core UI primitives. +- **Tailwind vs CSS classes:** Use Tailwind for one-off layout and spacing. Add a class in `globals.css` when a pattern is theme-sensitive, uses pseudo-elements, gradients, or repeats 3+ times. +- **Theme handling:** `:root` holds light tokens and `.dark` holds dark overrides. Use tokenized classes like `bg-[var(--color-bg-surface)]`, not `bg-white dark:bg-[#1a1918]`. + +## Token Mapping +These are the concrete token names used in `packages/web/src/app/globals.css`. New UI code should reference these names directly. + +### Core Surface Tokens +| CSS Token | Meaning | +|-----------|---------| +| `--color-bg-base` | Page background | +| `--color-bg-surface` | Standard card/panel background | +| `--color-bg-elevated` | Elevated surfaces, popovers, modals | +| `--color-bg-elevated-hover` | Hover state for elevated surfaces | +| `--color-bg-subtle` | Subtle fill for chips, hovers, muted emphasis | +| `--color-bg-sidebar` | Sidebar-specific background | + +### Core Text and Border Tokens +| CSS Token | Meaning | +|-----------|---------| +| `--color-text-primary` | Primary headings/body copy | +| `--color-text-secondary` | Supporting text | +| `--color-text-tertiary` | Captions/placeholders | +| `--color-text-muted` | Low-emphasis meta text | +| `--color-text-inverse` | Text on accent or dark fills | +| `--color-border-subtle` | Hairline dividers | +| `--color-border-default` | Standard borders | +| `--color-border-strong` | Emphasized borders/focus-adjacent borders | + +### Accent and Utility Tokens +| CSS Token | Meaning | +|-----------|---------| +| `--color-accent` | Primary interactive accent | +| `--color-accent-hover` | Hover state for accent surfaces | +| `--color-accent-subtle` | Accent-tinted background | +| `--color-accent-blue` | Semantic blue alias | +| `--color-accent-green` | Semantic green alias | +| `--color-accent-yellow` | Semantic amber alias | +| `--color-accent-orange` | Semantic orange alias | +| `--color-accent-red` | Semantic red alias | +| `--color-tint-blue` | Blue pill/badge background | +| `--color-tint-green` | Green pill/badge background | +| `--color-tint-yellow` | Yellow pill/badge background | +| `--color-tint-orange` | Orange pill/badge background | +| `--color-tint-red` | Red pill/badge background | +| `--color-tint-neutral` | Neutral pill/badge background | + +### Status and Alert Tokens +| CSS Token | Meaning | +|-----------|---------| +| `--color-status-working` | Agent actively working | +| `--color-status-ready` | Ready/queued state | +| `--color-status-respond` | Human response needed | +| `--color-status-review` | Review-needed state | +| `--color-status-pending` | Pending/queued emphasis | +| `--color-status-merge` | Merge-ready/merged emphasis | +| `--color-status-idle` | Idle/inactive state | +| `--color-status-done` | Completed/receding state | +| `--color-status-error` | Error/broken state | +| `--color-ci-pass` | CI passing | +| `--color-ci-fail` | CI failing | +| `--color-alert-ci` / `--color-alert-ci-bg` | CI failure callout row | +| `--color-alert-review` / `--color-alert-review-bg` | Review-requested callout row | +| `--color-alert-changes` / `--color-alert-changes-bg` | Changes-requested callout row | +| `--color-alert-conflict` / `--color-alert-conflict-bg` | Merge-conflict callout row | +| `--color-alert-comment` / `--color-alert-comment-bg` | New-comment callout row | + +## Component Anatomy + +### Session Card +``` +┌─ 2px left border (status color) ─────────────────────┐ +│ ┌─ Card (bg-surface, 1px border-default, 2px radius) │ +│ │ Title (text-primary, 12px, weight 500) │ +│ │ Branch · PR # (mono, text-tertiary, 10px) │ +│ │ ┌─ Status pill ────────────────────┐ │ +│ │ │ ● dot (6px, status color) Label │ │ +│ │ └──────────────────────────────────┘ │ +│ │ inset 0 1px 0 rgba(255,255,255,0.03) (dark only) │ +│ └─────────────────────────────────────────────────────│ +└───────────────────────────────────────────────────────┘ +``` +- **Padding:** 10px 12px +- **Spacing:** 4px between title and meta, 6px between meta and status +- **Hover:** bg-elevated-hover, border-color transition 0.12s +- **Active:** scale(0.99), 80ms +- **Containment:** `contain: layout style paint` for 30+ card performance + +### Button States +| State | Primary | Secondary | Ghost | Danger | +|-------|---------|-----------|-------|--------| +| Rest | bg: accent, text: #121110 | bg: elevated, border: border-default | bg: transparent | bg: transparent, border: red/30% | +| Hover | bg: accent-hover | bg: elevated-hover, border: border-strong | bg: bg-subtle | bg: red/8%, border: red | +| Active | scale(0.97) | scale(0.97) | scale(0.97) | scale(0.97) | +| Focus | outline: 2px accent | outline: 2px accent | outline: 2px accent | outline: 2px accent | +| Disabled | opacity: 0.5, cursor: not-allowed | opacity: 0.5 | opacity: 0.5 | opacity: 0.5 | +- **Padding:** 8px 16px +- **Font:** Geist Sans, 13px, weight 500 +- **Border-radius:** 0 +- **Min touch target:** 44px height (add padding if needed) + +### Input Fields +| State | Appearance | +|-------|------------| +| Rest | bg: bg-base, border: border-default, text: text-primary | +| Placeholder | color: text-tertiary | +| Focus | border-color: accent, no outline (border IS the indicator) | +| Error | border-color: status-error, error message below in status-error color | +| Disabled | opacity: 0.5, cursor: not-allowed, bg: bg-subtle | +- **Padding:** 8px 12px +- **Font:** Geist Sans, 13px +- **Border-radius:** 0 + +### Status Pill +- **Layout:** inline-flex, center-aligned, gap 6px +- **Dot:** 6px circle, filled with status color +- **Text:** 11px, weight 600, text-secondary +- **Background:** bg-subtle +- **Padding:** 4px 10px +- **Border-radius:** 0 + +### Alert / Banner +- **Layout:** flex, padding 12px 16px +- **Left border:** 2px solid, colored by severity +- **Background:** status color at 6% opacity +- **Text:** status color, 13px +- **Border-radius:** 0 +- **Variants:** success (green), warning (amber), error (red), info (cyan) + +## Web Patterns +- **Mono data:** IDs, hashes, timestamps, branch names, and PR numbers should use `font-mono` with `10-11px` sizing and slightly wider tracking. +- **Status text:** Session/card status labels should stay mono and low-emphasis unless the status itself is the primary signal. +- **Alert rows:** Inline alert/callout rows inside cards should use a 2px left border plus paired foreground/background alert tokens. +- **Dividers:** Use `border-[var(--color-border-subtle)]` or `border-[var(--color-border-default)]` instead of ad hoc neutral grays. +- **Existing reusable components:** Prefer current primitives/components like `ActivityDot`, `CIBadge`, `PRStatus`, `Toast`, and shared layout patterns already in `packages/web/src/components/`. +- **Sharp edges remain the rule:** Do not reintroduce rounded cards/buttons as a package-level convention. `rounded-full` is reserved for dots, pills, and avatars. + ## Performance Guidelines - Use `contain: layout style paint` and `content-visibility: auto` on session cards - Animate only `transform` and `opacity` (GPU-composited). Never animate `padding`, `margin`, `height`, `width`, `border`, or `box-shadow`. @@ -143,15 +305,29 @@ - `scale(0)` entry animations — start from `scale(0.95)` with `opacity: 0` - `ease-in` on UI elements — use `ease-out` for responsiveness - Animations over 300ms on frequently-triggered UI elements +- Neutral gray surfaces (#111, #222) — always use warm-tinted variants +- Blue-white text (#eef3ff) — use cream (#f0ece8) to maintain warmth +- `outline: none` without a visible focus replacement ## Decisions Log | Date | Decision | Rationale | |------|----------|-----------| -| 2026-03-28 | Initial design system created | Created by /design-consultation with competitive research (Conductor.build, T3 Code, OpenAI Codex, Emdash) + 4 design voices (primary, Codex CLI, Claude subagent, Emil Kowalski design eng) | -| 2026-03-28 | Geist Sans + JetBrains Mono (2 fonts only) | Emil review: 4 fonts creates cognitive gear-shifts on scan-heavy dashboards. Two fonts, hierarchy through weight + tracking. | -| 2026-03-28 | Accent #5B7EF8 instead of Tailwind blue-500 | Emil review: stock Tailwind blue reads "default" and undermines Industrial Precision identity. Existing token-reference blue is more sophisticated. | -| 2026-03-28 | Dual accent (cool blue + warm amber) | All 3 outside voices independently proposed amber for attention states. Two accents with clear semantic roles beat a single color. | -| 2026-03-28 | Fixed-width kanban columns (not variable) | Emil review: variable width breaks spatial memory. Encode urgency in column header dots instead. | -| 2026-03-28 | 2px base border-radius | Full 0px risks looking unstyled. 2px reads as intentionally sharp while feeling designed. Consistent across cards, buttons, inputs. Distinct from competitors' 8-12px rounded corners. | -| 2026-03-28 | Keep dot pulse, remove border heartbeat | Emil review: 4s border animation on 15+ cards is "decorative anxiety" with high perf cost. Existing 8px dot pulse is correct pattern — small surface, GPU-composited. | -| 2026-03-28 | Simplify ready-to-merge to single animation | Emil review: 3 concurrent keyframe animations on merge-ready cards is over-engineered. One animation per element, one purpose. Keep dot pulse only. | +| 2026-03-28 | Initial design system created | Created by /design-consultation with competitive research (Conductor.build, T3 Code, OpenAI Codex, Emdash) + 4 design voices | +| 2026-03-28 | Geist Sans + JetBrains Mono (2 fonts only) | Emil review: 4 fonts creates cognitive gear-shifts on scan-heavy dashboards | +| 2026-03-28 | 2px base border-radius (v1) | Full 0px risks looking unstyled. 2px reads as intentionally sharp while feeling designed. | +| 2026-04-05 | 0px border-radius everywhere | Hard edges are the identity. With warm surfaces and inset highlights providing depth, rounding adds nothing. Zero radius is the most honest expression of Industrial/Warm Terminal. | +| 2026-03-28 | Keep dot pulse, remove border heartbeat | Emil review: 4s border animation on 15+ cards is "decorative anxiety" with high perf cost. | +| 2026-04-05 | Fresh design system: Warm Terminal | Every competitor converges on cool blue-gray. Warm charcoal with cream text and warm periwinkle accent creates instant visual distinction. | +| 2026-04-05 | JetBrains Mono for display + data | Mono headlines in a mono-heavy dashboard create typographic cohesion instead of two competing voices. Free, open source, already in the codebase. | +| 2026-04-05 | Warm periwinkle #8b9cf7 accent (not gold) | Gold collides semantically with amber attention state. Blue = clickable is muscle memory. Warm periwinkle fits the palette without signal confusion. | +| 2026-04-05 | Brown-tinted surfaces, not neutral or blue-tinted | #121110 / #1a1918 / #222120 — warm undertone sets AO apart from every Linear clone. Light mode uses warm parchment #f5f3f0. | +| 2026-04-05 | Added accessibility section | Missing from v1. Touch targets 44px min, WCAG AA contrast, focus-visible, prefers-reduced-motion. | +| 2026-04-05 | Added component anatomy section | Missing from v1. Button states, input states, card structure, status pill, alert anatomy. | +| 2026-04-05 | Added light mode rationale | v1 listed values without explaining why. Warm parchment base, white card float, desaturated accent. | +| 2026-04-07 | `--status-respond` is amber, never red | Critique found Respond column using `--status-error` (red). Respond = human decision needed, not error. Separate token. See status colors table. | +| 2026-04-07 | Two-stage delete confirmation pattern | P0 safety: trash button first click enters amber "kill?" state for 2s; second click confirms. No modal. In-place via CSS `::after` + JS class toggle. Prevents accidental agent termination. | +| 2026-04-07 | Working card titles at full weight | P2: Working state is the primary operational state. Never dim active card titles. Dimming is reserved for Done/archived cards only. | +| 2026-04-07 | No current-project group label in sidebar sessions | P2: Current project label in the sessions list is redundant — the project switcher chip 12px above already names the project. Only other projects need group labels. | +| 2026-04-07 | Remove column shadows | P3: `18px/42px` column box-shadow created competing depth layers with card shadows. Border + background contrast does separation. No column-level shadow needed. | +| 2026-04-07 | Topbar shows page name only, not project name | Minor: Topbar "vinesight-rn / kanban" duplicated project name visible in sidebar. Topbar now shows "Kanban" + freshness. | +| 2026-04-07 | Diff size badges use `` with tooltip | Minor: S/M/L diff badges were opaque. `` gives meaning on hover without adding visual noise. | diff --git a/packages/core/package.json b/packages/core/package.json index 9c1c99bcc..e760f4a77 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -11,6 +11,42 @@ "types": "./dist/index.d.ts", "import": "./dist/index.js" }, + "./activity-log": { + "types": "./dist/activity-log.d.ts", + "import": "./dist/activity-log.js" + }, + "./config": { + "types": "./dist/config.d.ts", + "import": "./dist/config.js" + }, + "./decomposer": { + "types": "./dist/decomposer.d.ts", + "import": "./dist/decomposer.js" + }, + "./lifecycle-manager": { + "types": "./dist/lifecycle-manager.d.ts", + "import": "./dist/lifecycle-manager.js" + }, + "./metadata": { + "types": "./dist/metadata.d.ts", + "import": "./dist/metadata.js" + }, + "./observability": { + "types": "./dist/observability.d.ts", + "import": "./dist/observability.js" + }, + "./orchestrator-prompt": { + "types": "./dist/orchestrator-prompt.d.ts", + "import": "./dist/orchestrator-prompt.js" + }, + "./paths": { + "types": "./dist/paths.d.ts", + "import": "./dist/paths.js" + }, + "./plugin-registry": { + "types": "./dist/plugin-registry.d.ts", + "import": "./dist/plugin-registry.js" + }, "./types": { "types": "./dist/types.d.ts", "import": "./dist/types.js" @@ -22,6 +58,10 @@ "./scm-webhook-utils": { "types": "./dist/scm-webhook-utils.d.ts", "import": "./dist/scm-webhook-utils.js" + }, + "./session-manager": { + "types": "./dist/session-manager.d.ts", + "import": "./dist/session-manager.js" } }, "files": [ diff --git a/packages/core/src/__tests__/lifecycle-manager.test.ts b/packages/core/src/__tests__/lifecycle-manager.test.ts index 1298d9edb..01e6a4b1b 100644 --- a/packages/core/src/__tests__/lifecycle-manager.test.ts +++ b/packages/core/src/__tests__/lifecycle-manager.test.ts @@ -51,16 +51,32 @@ function setupCheck( configOverride?: OrchestratorConfig; }, ) { - vi.mocked(mockSessionManager.get).mockResolvedValue(opts.session); - - writeMetadata(env.sessionsDir, sessionId, { + const persistedMetadata = { worktree: "/tmp", branch: opts.session.branch ?? "main", status: opts.session.status, project: "my-app", + runtimeHandle: opts.session.runtimeHandle + ? JSON.stringify(opts.session.runtimeHandle) + : undefined, ...opts.metaOverrides, + }; + const persistedStringMetadata = Object.fromEntries( + Object.entries(persistedMetadata).filter( + (entry): entry is [string, string] => typeof entry[1] === "string", + ), + ); + + vi.mocked(mockSessionManager.get).mockResolvedValue({ + ...opts.session, + metadata: { + ...opts.session.metadata, + ...persistedStringMetadata, + }, }); + writeMetadata(env.sessionsDir, sessionId, persistedMetadata); + return createLifecycleManager({ config: opts.configOverride ?? config, registry: opts.registry ?? mockRegistry, @@ -98,6 +114,52 @@ describe("check (single session)", () => { expect(meta!["status"]).toBe("working"); }); + it("does not kill a spawning session when its runtime handle has not been persisted yet", async () => { + vi.mocked(plugins.runtime.isAlive).mockResolvedValue(false); + + const lm = setupCheck("app-1", { + session: makeSession({ + status: "spawning", + runtimeHandle: { id: "app-1", runtimeName: "mock", data: {} }, + metadata: {}, + }), + metaOverrides: { + runtimeHandle: undefined, + tmuxName: undefined, + }, + }); + + await lm.check("app-1"); + + expect(lm.getStates().get("app-1")).toBe("working"); + expect(plugins.runtime.isAlive).not.toHaveBeenCalled(); + }); + + it("still probes a working session when it relies on a synthesized runtime handle", async () => { + vi.mocked(plugins.runtime.isAlive).mockResolvedValue(false); + + const lm = setupCheck("app-1", { + session: makeSession({ + status: "working", + runtimeHandle: { id: "app-1", runtimeName: "mock", data: {} }, + metadata: {}, + }), + metaOverrides: { + runtimeHandle: undefined, + tmuxName: undefined, + }, + }); + + await lm.check("app-1"); + + expect(plugins.runtime.isAlive).toHaveBeenCalledWith({ + id: "app-1", + runtimeName: "mock", + data: {}, + }); + expect(lm.getStates().get("app-1")).toBe("killed"); + }); + it("uses worker-specific agent fallback when metadata does not persist an agent", async () => { const codexAgent: Agent = { ...plugins.agent, diff --git a/packages/core/src/lifecycle-manager.ts b/packages/core/src/lifecycle-manager.ts index 613d86156..99105fd42 100644 --- a/packages/core/src/lifecycle-manager.ts +++ b/packages/core/src/lifecycle-manager.ts @@ -371,9 +371,14 @@ export function createLifecycleManager(deps: LifecycleManagerDeps): LifecycleMan // Track activity state across steps so stuck detection can run after PR checks let detectedIdleTimestamp: Date | null = null; + const hasPersistedRuntimeIdentity = + typeof session.metadata["runtimeHandle"] === "string" || + typeof session.metadata["tmuxName"] === "string"; + const canProbeRuntimeIdentity = + hasPersistedRuntimeIdentity || session.status !== SESSION_STATUS.SPAWNING; // 1. Check if runtime is alive - if (session.runtimeHandle) { + if (session.runtimeHandle && canProbeRuntimeIdentity) { const runtime = registry.get("runtime", project.runtime ?? config.defaults.runtime); if (runtime) { const alive = await runtime.isAlive(session.runtimeHandle).catch(() => true); @@ -381,12 +386,17 @@ export function createLifecycleManager(deps: LifecycleManagerDeps): LifecycleMan } } - // 2. Check agent activity — prefer JSONL-based detection (runtime-agnostic) - if (agent && session.runtimeHandle) { + // 2. Check agent activity. + // JSONL-based activity detection is runtime-agnostic, but terminal probing + // and process checks should only run once a real runtime identity has been + // persisted. During spawn, sessionManager may fabricate a temporary handle + // to keep the object shape stable; treating that as a real tmux target can + // falsely mark a just-reserved session as killed before launch completes. + if (agent && (session.runtimeHandle || session.workspacePath)) { try { // If the agent implements recordActivity, capture terminal output and record // BEFORE calling getActivityState so the JSONL has fresh data to read. - if (agent.recordActivity && session.workspacePath) { + if (agent.recordActivity && session.workspacePath && session.runtimeHandle && canProbeRuntimeIdentity) { try { const runtime = registry.get( "runtime", @@ -420,17 +430,19 @@ export function createLifecycleManager(deps: LifecycleManagerDeps): LifecycleMan // proceed to PR checks below } else { // getActivityState returned null — fall back to terminal output parsing - const runtime = registry.get( - "runtime", - project.runtime ?? config.defaults.runtime, - ); - const terminalOutput = runtime ? await runtime.getOutput(session.runtimeHandle, 10) : ""; - if (terminalOutput) { - const activity = agent.detectActivity(terminalOutput); - if (activity === "waiting_input") return "needs_input"; + if (session.runtimeHandle && canProbeRuntimeIdentity) { + const runtime = registry.get( + "runtime", + project.runtime ?? config.defaults.runtime, + ); + const terminalOutput = runtime ? await runtime.getOutput(session.runtimeHandle, 10) : ""; + if (terminalOutput) { + const activity = agent.detectActivity(terminalOutput); + if (activity === "waiting_input") return "needs_input"; - const processAlive = await agent.isProcessRunning(session.runtimeHandle); - if (!processAlive) return "killed"; + const processAlive = await agent.isProcessRunning(session.runtimeHandle); + if (!processAlive) return "killed"; + } } } } catch { diff --git a/packages/web/src/__tests__/components.test.tsx b/packages/web/src/__tests__/components.test.tsx index 49fd3cad6..982d467d4 100644 --- a/packages/web/src/__tests__/components.test.tsx +++ b/packages/web/src/__tests__/components.test.tsx @@ -288,7 +288,7 @@ describe("SessionCard", () => { }); const session = makeSession({ status: "ci_failed", activity: "idle", pr }); render(); - expect(screen.getByText("1 CI check failing")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "1 check failing" })).toBeInTheDocument(); }); it("shows CI status unknown when ciStatus is failing but no failed checks", () => { @@ -393,7 +393,7 @@ describe("SessionCard", () => { }); const session = makeSession({ activity: "idle", pr }); render(); - expect(screen.getByText("ask to fix")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Ask to fix" })).toBeInTheDocument(); }); it("shows action buttons even when agent is active", () => { @@ -412,13 +412,13 @@ describe("SessionCard", () => { }); const session = makeSession({ activity: "active", pr }); render(); - expect(screen.getByText("ask to fix")).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Ask to fix" })).toBeInTheDocument(); }); it("shows issue details in the compact card footer", () => { const session = makeSession({ id: "test-1", issueId: "INT-100", pr: null }); render(); - expect(screen.getAllByText("INT-100")).toHaveLength(2); + expect(screen.getAllByText("INT-100")).toHaveLength(1); }); it("shows icon-only terminate button in the footer", () => { @@ -548,7 +548,7 @@ describe("SessionCard", () => { render(); - const actionButton = screen.getByRole("button", { name: "ask to fix" }); + const actionButton = screen.getByRole("button", { name: "Ask to fix" }); fireEvent.click(actionButton); await waitFor(() => { @@ -558,7 +558,7 @@ describe("SessionCard", () => { await waitFor(() => { expect(screen.getByRole("button", { name: "failed" })).toBeInTheDocument(); }); - expect(screen.queryByRole("button", { name: "sent!" })).not.toBeInTheDocument(); + expect(screen.queryByRole("button", { name: "Sent!" })).not.toBeInTheDocument(); }); }); @@ -575,7 +575,26 @@ describe("AttentionZone", () => { it("renders empty state when sessions array is empty", () => { render(); - expect(screen.getByText("No sessions")).toBeInTheDocument(); + expect(screen.getByText("Respond")).toBeInTheDocument(); + expect(screen.getByText("0")).toBeInTheDocument(); + expect(screen.queryByText("No agents need your input.")).not.toBeInTheDocument(); + }); + + it("renders zone-specific empty messages for all attention zones", () => { + const cases: Array<[string, string]> = [ + ["review", "Review"], + ["pending", "Pending"], + ["working", "Working"], + ["done", "Done"], + ]; + for (const [level, expectedLabel] of cases) { + const { unmount } = render( + , + ); + expect(screen.getByText(expectedLabel)).toBeInTheDocument(); + expect(screen.getByText("0")).toBeInTheDocument(); + unmount(); + } }); it("shows session cards when not collapsed", () => { diff --git a/packages/web/src/__tests__/setup.ts b/packages/web/src/__tests__/setup.ts index 533b774d3..b3258b10f 100644 --- a/packages/web/src/__tests__/setup.ts +++ b/packages/web/src/__tests__/setup.ts @@ -1,4 +1,11 @@ -import "@testing-library/jest-dom/vitest"; +import { cleanup } from "@testing-library/react"; +import * as matchers from "@testing-library/jest-dom/matchers"; +import { afterEach, expect } from "vitest"; + +expect.extend(matchers); +afterEach(() => { + cleanup(); +}); // jsdom does not implement window.matchMedia. Provide a minimal stub so // components that call useMediaQuery (e.g. Dashboard) work in unit tests. diff --git a/packages/web/src/app/globals.css b/packages/web/src/app/globals.css index 9e57b0d2a..30f4d9f23 100644 --- a/packages/web/src/app/globals.css +++ b/packages/web/src/app/globals.css @@ -27,71 +27,84 @@ /* ── Light mode (default) ─────────────────────────────────────────── */ :root { /* Base surfaces — design-system light palette */ - --color-bg-base: #f5f5f7; + --color-bg-base: #f5f3f0; --color-bg-surface: #ffffff; - --color-bg-elevated: #ffffff; - --color-bg-elevated-hover: #f7f7f8; - --color-bg-subtle: #f0f0f2; + --color-bg-elevated: #f9f7f5; + --color-bg-elevated-hover: #f7f5f2; + --color-bg-subtle: rgba(120, 100, 80, 0.06); + --color-bg-hover: #f7f5f2; + --color-bg-inset: #f0ede9; + --color-bg-sidebar: #f0ede9; + --color-bg-card: #ffffff; /* Backward-compat aliases */ --color-bg-primary: var(--color-bg-base); --color-bg-secondary: var(--color-bg-surface); --color-bg-tertiary: var(--color-bg-elevated); - /* Borders */ - --color-border-subtle: #e7e7ec; - --color-border-default: #d9d9de; - --color-border-strong: #c9c9d2; + /* Borders — warm stone tint */ + --color-border-subtle: rgba(0, 0, 0, 0.06); + --color-border-default: #d6d3d1; + --color-border-strong: #c4bfba; /* Backward-compat aliases */ --color-border-muted: var(--color-border-subtle); --color-border-emphasis: var(--color-border-strong); - /* Text */ - --color-text-primary: #1b1b1f; - --color-text-secondary: #5e5e66; - --color-text-tertiary: #8e8e96; - --color-text-muted: #8e8e96; + /* Text — warm near-black and stone tones */ + --color-text-primary: #1c1917; + --color-text-secondary: #57534e; + --color-text-tertiary: #78716c; + --color-text-muted: #a8a29e; --color-text-inverse: #ffffff; - /* Interactive accent */ - --color-accent: #5B7EF8; - --color-accent-hover: #7a96ff; - --color-accent-subtle: rgba(91, 126, 248, 0.12); + /* Interactive accent — warm periwinkle, darkened for light mode contrast */ + --color-accent: #5c64b5; + --color-accent-hover: #4a52a0; + --color-accent-subtle: rgba(92, 100, 181, 0.1); + + /* Amber accent — orchestrator / CTA */ + --color-accent-amber: #d97706; + --color-accent-amber-dim: rgba(217, 119, 6, 0.1); + --color-accent-amber-border: rgba(217, 119, 6, 0.3); /* Status / semantic colors */ --color-status-working: #16a34a; - --color-status-ready: #5e6ad2; - --color-status-respond: #ca8a04; + --color-status-ready: #6b73c4; + --color-status-respond: #ea580c; --color-status-review: #0891b2; - --color-status-attention: #9a6700; - --color-status-idle: #8b949e; - --color-status-done: #d0d7de; + --color-status-pending: #ca8a04; + --color-status-attention: #b8860b; + --color-status-idle: #a8a29e; + --color-status-done: #a8a29e; --color-status-error: #dc2626; + --color-status-merge: #16a34a; + --color-ci-pass: #16a34a; + --color-ci-fail: #dc2626; /* Semantic aliases */ - --color-accent-blue: #5B7EF8; + --color-accent-blue: #2563eb; --color-accent-green: #16a34a; - --color-accent-yellow: #9a6700; + --color-accent-yellow: #ca8a04; --color-accent-orange: #bc4c00; --color-accent-red: #dc2626; - --color-accent-violet: #5B7EF8; - --color-accent-purple: #5B7EF8; + --color-accent-violet: #7c3aed; + --color-accent-purple: #7c3aed; /* Theme-adaptive tints (bg tints behind status pills) */ - --color-tint-blue: rgba(91, 126, 248, 0.08); - --color-tint-green: rgba(22, 163, 74, 0.08); - --color-tint-yellow: rgba(154, 103, 0, 0.08); - --color-tint-red: rgba(220, 38, 38, 0.08); - --color-tint-violet: rgba(91, 126, 248, 0.08); + --color-tint-blue: rgba(37, 99, 235, 0.08); + --color-tint-green: rgba(22, 163, 74, 0.07); + --color-tint-yellow: rgba(184, 134, 11, 0.07); + --color-tint-red: rgba(220, 38, 38, 0.07); + --color-tint-violet: rgba(92, 100, 181, 0.08); --color-tint-orange: rgba(188, 76, 0, 0.08); --color-tint-neutral: rgba(0, 0, 0, 0.04); /* Chip/badge background */ - --color-chip-bg: #f2f2f2; + --color-chip-bg: #f0ece8; /* Hover overlay */ - --color-hover-overlay: #f7f7f8; + --color-hover-overlay: #f7f5f2; /* Scrollbar */ --color-scrollbar: rgba(0, 0, 0, 0.08); @@ -105,32 +118,32 @@ /* Nav */ --color-nav-bg: #ffffff; - /* Card surfaces — clean white on light gray */ - --card-bg: #ffffff; - --card-merge-bg: #f6fbf8; - --card-expanded-bg: #f7f7f8; + /* Card surfaces — warm white on warm parchment */ + --card-bg: linear-gradient(180deg, #ffffff 0%, #faf9f7 100%); + --card-merge-bg: #f6faf5; + --card-expanded-bg: #f7f5f2; --card-shadow: none; --card-shadow-hover: 0 2px 6px rgba(0, 0, 0, 0.08); --card-inset: none; - --card-border: #d5d7de; + --card-border: #d6d3d1; /* Done card surfaces */ - --card-done-bg: #fafafa; - --card-done-border: #e8e8ec; + --card-done-bg: #faf9f7; + --card-done-border: #e7e5e4; - --done-pill-exited-bg: #f2f2f2; - --done-pill-exited-color: #8b8b93; + --done-pill-exited-bg: #f0ece8; + --done-pill-exited-color: #78716c; --done-pill-merged-bg: rgba(26, 127, 55, 0.08); --done-pill-merged-color: #1a7f37; --done-pill-killed-bg: rgba(207, 34, 46, 0.06); --done-pill-killed-color: #cf222e; - --done-meta-chip-bg: #f2f2f2; - --done-meta-chip-border: #e8e8ec; - --done-section-border: #ececf0; - --done-title-color: #5e5e66; - --done-restore-bg: rgba(9, 105, 218, 0.06); - --done-restore-border: rgba(9, 105, 218, 0.2); - --done-restore-hover-bg: rgba(9, 105, 218, 0.12); + --done-meta-chip-bg: #f0ece8; + --done-meta-chip-border: #e7e5e4; + --done-section-border: #e7e5e4; + --done-title-color: #57534e; + --done-restore-bg: rgba(92, 100, 181, 0.06); + --done-restore-border: rgba(92, 100, 181, 0.2); + --done-restore-hover-bg: rgba(92, 100, 181, 0.12); /* Detail card */ --detail-card-bg: #ffffff; @@ -142,22 +155,21 @@ --btn-inset: none; /* Kanban column */ - --color-column-bg: #f4f5f7; + --color-column-bg: #ece9e4; --color-column-header: transparent; /* Alert colors */ - --color-alert-ci: #6366f1; - --color-alert-ci-bg: #4f46e5; - --color-alert-ci-unknown: #9a6700; - --color-alert-review: #1a7f37; - --color-alert-review-bg: #1a7f37; - --color-alert-changes: #5B7EF8; - --color-alert-changes-bg: #5B7EF8; - --color-alert-conflict: #9a6700; - --color-alert-conflict-bg: #92400e; - --color-alert-comment: #bc4c00; - --color-alert-comment-bg: #c2410c; - + --color-alert-ci: #dc2626; + --color-alert-ci-bg: rgba(220,38,38,0.06); + --color-alert-ci-unknown: #b8860b; + --color-alert-review: #2563eb; + --color-alert-review-bg: rgba(37,99,235,0.06); + --color-alert-changes: #d97706; + --color-alert-changes-bg: rgba(217,119,6,0.06); + --color-alert-conflict: #ca8a04; + --color-alert-conflict-bg: rgba(202,138,4,0.06); + --color-alert-comment: #7c3aed; + --color-alert-comment-bg: rgba(124,58,237,0.06); } /* ── Dark mode ────────────────────────────────────────────────────── */ @@ -165,133 +177,145 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - /* Base surfaces — graphite dashboard */ - --color-bg-base: #0a0d12; - --color-bg-surface: #11161d; - --color-bg-elevated: #171d26; - --color-bg-elevated-hover: #1c2430; - --color-bg-subtle: rgba(177, 206, 255, 0.05); + /* Base surfaces — warm charcoal (brown-tinted, not blue/neutral) */ + --color-bg-base: #121110; + --color-bg-surface: #1a1918; + --color-bg-elevated: #222120; + --color-bg-elevated-hover: #2a2928; + --color-bg-subtle: rgba(255, 240, 220, 0.05); + --color-bg-hover: rgba(255, 240, 220, 0.08); + --color-bg-inset: #161514; + --color-bg-sidebar: #0e0d0c; + --color-bg-card: #1c1b19; - /* Borders */ - --color-border-subtle: rgba(160, 190, 255, 0.08); - --color-border-default: rgba(160, 190, 255, 0.14); - --color-border-strong: rgba(185, 214, 255, 0.24); + /* Borders — warm-tinted transparency */ + --color-border-subtle: rgba(255, 240, 220, 0.07); + --color-border-default: rgba(255, 240, 220, 0.13); + --color-border-strong: rgba(255, 240, 220, 0.24); - /* Text */ - --color-text-primary: #eef3ff; - --color-text-secondary: #a5afc4; - --color-text-tertiary: #6f7c94; - --color-text-muted: #6f7c94; - --color-text-inverse: #0a0d12; + /* Text — cream, not blue-white */ + --color-text-primary: #f0ece8; + --color-text-secondary: #a8a29e; + --color-text-tertiary: #78716c; + --color-text-muted: #57534e; + --color-text-inverse: #121110; - /* Interactive accent */ - --color-accent: #5B7EF8; - --color-accent-hover: #7a96ff; - --color-accent-subtle: rgba(91, 126, 248, 0.12); + /* Interactive accent — warm periwinkle */ + --color-accent: #8b9cf7; + --color-accent-hover: #a3b1fa; + --color-accent-subtle: rgba(139, 156, 247, 0.12); + + /* Amber accent — orchestrator / CTA */ + --color-accent-amber: #f97316; + --color-accent-amber-dim: rgba(249, 115, 22, 0.12); + --color-accent-amber-border: rgba(249, 115, 22, 0.35); /* Status */ --color-status-working: #22c55e; - --color-status-ready: #5B7EF8; - --color-status-respond: #f1be64; + --color-status-ready: #8b9cf7; + --color-status-respond: #ea580c; --color-status-review: #06b6d4; - --color-status-attention: #f1be64; - --color-status-idle: #293142; - --color-status-done: #3a4252; + --color-status-pending: #eab308; + --color-status-attention: #e2a336; + --color-status-idle: #44403c; + --color-status-done: #44403c; --color-status-error: #ef4444; + --color-status-merge: #22c55e; + --color-ci-pass: #22c55e; + --color-ci-fail: #ef4444; /* Semantic aliases */ - --color-accent-blue: #5B7EF8; + --color-accent-blue: #60a5fa; --color-accent-green: #22c55e; - --color-accent-yellow: #f1be64; + --color-accent-yellow: #eab308; --color-accent-orange: #ff9d57; --color-accent-red: #ef4444; - --color-accent-violet: #5B7EF8; - --color-accent-purple: #5B7EF8; + --color-accent-violet: #a78bfa; + --color-accent-purple: #a78bfa; /* Theme-adaptive tints */ - --color-tint-blue: rgba(91, 126, 248, 0.12); + --color-tint-blue: rgba(96, 165, 250, 0.12); --color-tint-green: rgba(34, 197, 94, 0.12); - --color-tint-yellow: rgba(241, 190, 100, 0.12); + --color-tint-yellow: rgba(226, 163, 54, 0.12); --color-tint-red: rgba(239, 68, 68, 0.12); - --color-tint-violet: rgba(91, 126, 248, 0.12); + --color-tint-violet: rgba(139, 156, 247, 0.12); --color-tint-orange: rgba(255, 157, 87, 0.12); --color-tint-neutral: rgba(255, 255, 255, 0.05); /* Chip/badge background */ - --color-chip-bg: rgba(255, 255, 255, 0.06); + --color-chip-bg: rgba(255, 240, 220, 0.06); /* Hover overlay */ - --color-hover-overlay: rgba(143, 180, 255, 0.05); + --color-hover-overlay: rgba(255, 240, 220, 0.05); /* Scrollbar */ --color-scrollbar: rgba(255, 255, 255, 0.08); --color-scrollbar-hover: rgba(255, 255, 255, 0.15); --color-scrollbar-active: rgba(255, 255, 255, 0.25); - /* Body */ - --color-body-gradient-blue: rgba(110, 143, 255, 0.2); - --color-body-gradient-violet: rgba(91, 126, 248, 0.08); + /* Body — warm ambient glow, not blue radials */ + --color-body-gradient-blue: rgba(139, 156, 247, 0.08); + --color-body-gradient-violet: rgba(139, 120, 100, 0.06); /* Nav glass */ - --color-nav-bg: rgba(10, 13, 18, 0.82); + --color-nav-bg: rgba(18, 17, 16, 0.82); - /* Card surfaces */ - --card-bg: linear-gradient(180deg, rgba(22, 28, 37, 0.98) 0%, rgba(16, 21, 29, 0.98) 100%); - --card-merge-bg: rgba(17, 23, 31, 0.98); + /* Card surfaces — match cards.html dark reference */ + --card-bg: var(--color-bg-card); + --card-merge-bg: rgba(20, 22, 18, 0.98); --card-expanded-bg: linear-gradient( 180deg, - rgba(26, 34, 45, 0.98) 0%, - rgba(18, 24, 33, 0.98) 100% + rgba(34, 33, 32, 0.98) 0%, + rgba(26, 25, 24, 0.98) 100% ); - --card-shadow: 0 18px 36px rgba(2, 6, 12, 0.24); - --card-shadow-hover: 0 24px 54px rgba(2, 6, 12, 0.34); - --card-inset: inset 0 1px 0 rgba(255, 255, 255, 0.04); - --card-border: rgba(166, 190, 226, 0.18); + --card-shadow: 0 18px 36px rgba(0, 0, 0, 0.24); + --card-shadow-hover: 0 24px 54px rgba(0, 0, 0, 0.34); + --card-inset: inset 0 1px 0 rgba(255, 255, 255, 0.03); + --card-border: var(--color-border-subtle); /* Done card surfaces */ - --card-done-bg: linear-gradient(180deg, rgba(18, 24, 33, 0.72) 0%, rgba(14, 19, 27, 0.72) 100%); - --card-done-border: rgba(160, 190, 255, 0.1); + --card-done-bg: linear-gradient(180deg, rgba(30, 29, 28, 0.8) 0%, rgba(24, 23, 22, 0.8) 100%); + --card-done-border: rgba(255, 240, 220, 0.12); - --done-pill-exited-bg: rgba(143, 180, 255, 0.08); - --done-pill-exited-color: #8a9ab6; + --done-pill-exited-bg: rgba(255, 240, 220, 0.06); + --done-pill-exited-color: #a8a29e; --done-pill-merged-bg: rgba(95, 211, 154, 0.14); --done-pill-merged-color: #5fd39a; --done-pill-killed-bg: rgba(255, 123, 114, 0.12); --done-pill-killed-color: #ff7b72; - --done-meta-chip-bg: rgba(143, 180, 255, 0.05); - --done-meta-chip-border: rgba(160, 190, 255, 0.1); - --done-section-border: rgba(160, 190, 255, 0.08); - --done-title-color: #9ba8bf; - --done-restore-bg: rgba(143, 180, 255, 0.1); - --done-restore-border: rgba(143, 180, 255, 0.24); - --done-restore-hover-bg: rgba(143, 180, 255, 0.18); + --done-meta-chip-bg: rgba(255, 240, 220, 0.04); + --done-meta-chip-border: rgba(255, 240, 220, 0.08); + --done-section-border: rgba(255, 240, 220, 0.06); + --done-title-color: #a8a29e; + --done-restore-bg: rgba(139, 156, 247, 0.1); + --done-restore-border: rgba(139, 156, 247, 0.24); + --done-restore-hover-bg: rgba(139, 156, 247, 0.18); /* Detail card */ - --detail-card-bg: linear-gradient(180deg, rgba(20, 26, 35, 0.98) 0%, rgba(14, 19, 27, 0.98) 100%); + --detail-card-bg: linear-gradient(180deg, rgba(26, 25, 24, 0.98) 0%, rgba(18, 17, 16, 0.98) 100%); --detail-card-shadow: 0 18px 42px rgba(0, 0, 0, 0.28); /* Orchestrator button */ - --btn-shadow: 0 10px 24px rgba(34, 63, 116, 0.16); - --btn-shadow-hover: 0 16px 34px rgba(34, 63, 116, 0.22); - --btn-inset: inset 0 1px 0 rgba(255, 255, 255, 0.08); + --btn-shadow: 0 10px 24px rgba(0, 0, 0, 0.16); + --btn-shadow-hover: 0 16px 34px rgba(0, 0, 0, 0.22); + --btn-inset: inset 0 1px 0 rgba(255, 255, 255, 0.06); /* Kanban column */ - --color-column-bg: #0d1218; + --color-column-bg: var(--color-bg-subtle); --color-column-header: transparent; - /* Alert colors — Linear-muted */ - --color-alert-ci: #7b85e0; - --color-alert-ci-bg: rgba(94, 106, 210, 0.25); - --color-alert-ci-unknown: #d4a72c; - --color-alert-review: #4dab6e; - --color-alert-review-bg: rgba(77, 171, 110, 0.25); - --color-alert-changes: #5B7EF8; - --color-alert-changes-bg: rgba(91, 126, 248, 0.25); - --color-alert-conflict: #d4a72c; - --color-alert-conflict-bg: rgba(212, 167, 44, 0.25); - --color-alert-comment: #c88a2e; - --color-alert-comment-bg: rgba(200, 138, 46, 0.25); - + /* Alert colors — warm-tinted */ + --color-alert-ci: #ef4444; + --color-alert-ci-bg: rgba(239,68,68,0.08); + --color-alert-ci-unknown: #e2a336; + --color-alert-review: #60a5fa; + --color-alert-review-bg: rgba(96,165,250,0.08); + --color-alert-changes: #f97316; + --color-alert-changes-bg: rgba(249,115,22,0.08); + --color-alert-conflict: #eab308; + --color-alert-conflict-bg: rgba(234,179,8,0.08); + --color-alert-comment: #a78bfa; + --color-alert-comment-bg: rgba(167,139,250,0.08); } @theme { @@ -300,7 +324,7 @@ --font-mono: var(--font-jetbrains-mono), "SF Mono", "Menlo", "Consolas", monospace; /* ── Border radius ───────────────────────────────────────────────── */ - --radius-base: 2px; + --radius-base: 0; --radius-sm: 4px; --radius-md: 6px; --radius-lg: 8px; @@ -353,36 +377,36 @@ 0 4px 8px rgba(0, 0, 0, 0.85), 0 14px 36px rgba(0, 0, 0, 0.55), inset 0 1px 0 rgba(255, 255, 255, 0.1); - /* Gradient surfaces */ + /* Gradient surfaces — warm tints */ --gradient-session-card: linear-gradient( 175deg, - rgba(30, 30, 35, 1) 0%, - rgba(23, 23, 27, 1) 100% + rgba(30, 29, 28, 1) 0%, + rgba(23, 22, 21, 1) 100% ); --gradient-session-card-expanded: linear-gradient( 175deg, - rgba(38, 38, 44, 1) 0%, - rgba(30, 30, 35, 1) 100% + rgba(38, 37, 36, 1) 0%, + rgba(30, 29, 28, 1) 100% ); --gradient-session-card-merge: linear-gradient( 175deg, - rgba(23, 29, 25, 1) 0%, - rgba(17, 22, 19, 1) 100% + rgba(23, 25, 22, 1) 0%, + rgba(17, 18, 16, 1) 100% ); - --gradient-detail-card: linear-gradient(175deg, rgba(30, 30, 35, 1) 0%, rgba(23, 23, 27, 1) 100%); + --gradient-detail-card: linear-gradient(175deg, rgba(30, 29, 28, 1) 0%, rgba(23, 22, 21, 1) 100%); --gradient-orchestrator-btn: linear-gradient( 175deg, - rgba(94, 106, 210, 0.12) 0%, - rgba(94, 106, 210, 0.06) 100% + rgba(139, 156, 247, 0.12) 0%, + rgba(139, 156, 247, 0.06) 100% ); --gradient-orchestrator-btn-hover: linear-gradient( 175deg, - rgba(94, 106, 210, 0.18) 0%, - rgba(94, 106, 210, 0.1) 100% + rgba(139, 156, 247, 0.18) 0%, + rgba(139, 156, 247, 0.1) 100% ); --gradient-status-strip: linear-gradient( to bottom, - rgba(94, 106, 210, 0.04) 0%, + rgba(139, 156, 247, 0.04) 0%, transparent 100% ); } @@ -495,6 +519,173 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { background: rgba(0, 0, 0, 0.4); } +/* ── Terminal exited placeholder ─────────────────────────────────────── */ + +.terminal-exited-placeholder { + display: flex; + align-items: center; + justify-content: center; + background: var(--color-bg-inset); + border: 1px solid var(--color-border-subtle); + border-radius: var(--radius-lg); +} + +.terminal-exited-placeholder__text { + color: var(--color-text-tertiary); + font-size: 13px; +} + +/* ── Empty State + Ghost Kanban ──────────────────────────────────────── */ + +.board-wrapper { + position: relative; + flex: 1; + overflow: hidden; +} + +.kanban-ghost { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 6px; + position: absolute; + inset: 10px 0 0; + pointer-events: none; +} + +.kanban-ghost__col { + border-radius: 7px; + border: 1px dashed var(--color-border-subtle); + display: flex; + flex-direction: column; + opacity: 0.7; +} + +.kanban-ghost__head { + padding: 9px 9px 7px; + border-bottom: 1px solid var(--color-border-subtle); + font-size: 10.5px; + font-weight: 500; + color: var(--color-text-muted); + letter-spacing: 0.05em; + text-transform: uppercase; + font-family: var(--font-mono); +} + +.dark .kanban-ghost__col { + border-color: rgba(255, 240, 220, 0.08); +} + +.dark .kanban-ghost__head { + border-bottom-color: rgba(255, 240, 220, 0.07); +} + +.board-center { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + transform: translateY(-12px); +} + +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: 28px 20px; +} + +@keyframes empty-state-enter { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.empty-state__icon { + width: 50px; + height: 50px; + border-radius: 50%; + background: var(--color-bg-subtle); + border: 1px solid var(--color-border-subtle); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 14px; + animation: empty-state-enter 280ms cubic-bezier(0.23, 1, 0.32, 1) both; +} + +.empty-state__headline { + font-size: 13px; + font-weight: 500; + color: var(--color-text-secondary); + letter-spacing: -0.01em; + margin-bottom: 5px; + animation: empty-state-enter 280ms cubic-bezier(0.23, 1, 0.32, 1) 45ms both; +} + +.empty-state__hint { + font-size: 12px; + color: var(--color-text-muted); + max-width: 210px; + line-height: 1.65; + margin-bottom: 18px; + animation: empty-state-enter 280ms cubic-bezier(0.23, 1, 0.32, 1) 90ms both; +} + +.empty-state__text { + font-size: 13px; + color: var(--color-text-muted); + animation: empty-state-enter 280ms cubic-bezier(0.23, 1, 0.32, 1) 40ms both; +} + +.empty-state__cta { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 0 12px; + height: 29px; + border-radius: 5px; + border: 1px solid var(--color-border-strong); + background: transparent; + color: var(--color-text-secondary); + font-family: var(--font-sans); + font-size: 12px; + font-weight: 500; + cursor: pointer; + text-decoration: none; + transition: background 100ms ease, border-color 100ms ease, color 100ms ease, + transform 120ms cubic-bezier(0.23, 1, 0.32, 1); + animation: empty-state-enter 280ms cubic-bezier(0.23, 1, 0.32, 1) 135ms both; +} + +.empty-state__cta:hover { + background: var(--color-bg-subtle); + border-color: var(--color-border-strong); + color: var(--color-text-primary); + text-decoration: none; +} + +.empty-state__cta:active { + transform: scale(0.97); +} + +@media (prefers-reduced-motion: reduce) { + .empty-state__icon, + .empty-state__headline, + .empty-state__hint, + .empty-state__text, + .empty-state__cta { + animation: none; + } +} + /* ── Animations ──────────────────────────────────────────────────────── */ @keyframes progress-shimmer { @@ -588,16 +779,43 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { animation: dot-ring 2s ease-in-out infinite; } -/* ── Kanban card entrance ─────────────────────────────────────────────── */ +/* ── Kanban card entrance — Emil staggered animation ─────────────────── */ .kanban-card-enter { - animation: slide-up 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards; + animation: card-enter 0.2s cubic-bezier(0.16, 1, 0.3, 1) both; +} + +.kanban-column-body .kanban-card-enter:nth-child(1) { + animation-delay: 0ms; +} +.kanban-column-body .kanban-card-enter:nth-child(2) { + animation-delay: 40ms; +} +.kanban-column-body .kanban-card-enter:nth-child(3) { + animation-delay: 80ms; +} +.kanban-column-body .kanban-card-enter:nth-child(4) { + animation-delay: 120ms; +} +.kanban-column-body .kanban-card-enter:nth-child(5) { + animation-delay: 160ms; +} + +@keyframes card-enter { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: translateY(0); + } } /* ── Orchestrator button ──────────────────────────────────────────────── */ .orchestrator-btn { - border-radius: 2px; + border-radius: 0; color: var(--color-accent); background: linear-gradient( 175deg, @@ -643,26 +861,251 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { position: relative; } +.dashboard-app-shell { + display: flex; + min-height: 100vh; + flex-direction: column; + background: var(--color-bg-base); +} + +.dashboard-app-header { + display: flex; + height: 48px; + align-items: center; + gap: 10px; + padding: 0 14px; + border-bottom: 1px solid var(--color-border-subtle); + background: var(--color-bg-base); + flex-shrink: 0; + z-index: 10; +} + +.dashboard-app-sidebar-toggle { + width: 28px; + height: 28px; + border: none; + border-radius: 5px; + background: transparent; + color: var(--color-text-muted); + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + flex-shrink: 0; + transition: background 100ms, color 100ms; +} + +.dashboard-app-sidebar-toggle:hover { + background: var(--color-bg-subtle); + color: var(--color-text-secondary); +} + +.dashboard-app-header__brand { + display: inline-flex; + align-items: center; + gap: 7px; + font-size: 13px; + font-weight: 500; + letter-spacing: -0.01em; + color: var(--color-text-primary); +} + +.dashboard-app-header__brand-dot { + width: 7px; + height: 7px; + border-radius: 999px; + flex-shrink: 0; + background: #22c55e; + box-shadow: 0 0 6px rgba(34, 197, 94, 0.55); +} + +.dashboard-app-header__sep { + width: 1px; + height: 16px; + margin: 0 2px; + background: var(--color-border-default); +} + +.dashboard-app-header__project { + font-size: 12px; + font-weight: 400; + color: var(--color-text-secondary); +} + +.dashboard-app-header__spacer { + flex: 1; +} + +.dashboard-app-header__actions { + display: flex; + align-items: center; + gap: 6px; +} + +.dashboard-app-btn { + display: inline-flex; + align-items: center; + gap: 5px; + height: 27px; + padding: 0 10px; + border: 1px solid var(--color-border-default); + border-radius: 5px; + background: transparent; + color: var(--color-text-secondary); + text-decoration: none; + font-size: 12px; + font-weight: 500; + transition: + background 100ms ease, + border-color 100ms ease, + color 100ms ease, + transform 120ms cubic-bezier(0.23, 1, 0.32, 1); +} + +.dashboard-app-btn:hover { + background: var(--color-bg-subtle); + border-color: var(--color-border-strong); + color: var(--color-text-primary); + text-decoration: none; +} + +.dashboard-app-btn:active { + transform: scale(0.97); +} + +.dashboard-app-btn--amber { + border-color: var(--color-accent-amber-border); + background: var(--color-accent-amber-dim); + color: var(--color-accent-amber); +} + +.dashboard-app-btn--amber:hover { + background: color-mix(in srgb, var(--color-accent-amber) 20%, transparent); + color: var(--color-accent-amber); +} + +.dashboard-shell--desktop { + display: grid; + grid-template-columns: auto 1fr; + flex: 1; + min-height: 0; + overflow: hidden; +} + +.dashboard-shell--desktop.dashboard-shell--sidebar-collapsed { + grid-template-columns: 0 minmax(0, 1fr); +} + +.dashboard-shell--desktop > .project-sidebar { + width: 224px; + height: auto; + min-height: 0; + align-self: stretch; + flex-shrink: 0; +} + +.dashboard-shell--desktop > .project-sidebar.project-sidebar--collapsed { + width: 0; + border-right-color: transparent; +} + +.dashboard-shell--desktop > .dashboard-main--desktop { + min-width: 0; + align-self: stretch; +} + .dashboard-main { position: relative; } -.dashboard-main::before { - content: ""; - position: absolute; - inset: 0 0 auto 0; - height: 140px; - pointer-events: none; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.015) 0%, transparent 100%); +.dashboard-main--desktop { + display: flex; + flex: 1; + min-height: 0; + flex-direction: column; + overflow: hidden; + background: var(--color-bg-base); +} + +.dashboard-main__subhead { + padding: 14px 16px 0; + flex-shrink: 0; +} + +.dashboard-main__title { + font-size: 15px; + font-weight: 600; + letter-spacing: -0.02em; + color: var(--color-text-primary); +} + +.dashboard-main__subtitle { + margin-top: 1px; + font-size: 12px; + color: var(--color-text-muted); +} + +.dashboard-main__body { + display: flex; + flex: 1; + min-height: 0; + flex-direction: column; + overflow-y: auto; + padding: 0 16px 16px; +} + +.dashboard-main__body .kanban-board-wrap, +.dashboard-main__body .board-wrapper { + flex: 1; + min-height: 0; +} + +.project-sidebar__footer { + padding: 8px 12px; + border-top: 1px solid var(--sidebar-border); + display: flex; + align-items: center; + flex-shrink: 0; +} + +.project-sidebar__footer-btn { + width: 100%; + height: 29px; + padding: 0 10px; + border-radius: 5px; + border: 1px solid transparent; + background: transparent; + color: var(--color-text-muted); + display: inline-flex; + align-items: center; + justify-content: flex-start; + gap: 8px; + font-size: 11px; + font-weight: 500; + letter-spacing: -0.01em; + transition: background 100ms, color 100ms, border-color 100ms; +} + +.project-sidebar__footer-btn:hover { + background: var(--sidebar-row-hover); + color: var(--color-text-secondary); + border-color: color-mix(in srgb, var(--color-border-default) 72%, transparent); +} + +.project-sidebar__footer-btn svg { + width: 15px; + height: 15px; + flex-shrink: 0; } .dashboard-hero { position: relative; overflow: visible; - border: 1px solid var(--color-border-default); - border-radius: 2px; - background: var(--color-bg-surface); - box-shadow: var(--card-shadow); + border: none; + border-bottom: 1px solid color-mix(in srgb, var(--color-border-default) 92%, transparent); + border-radius: 0; + background: transparent; + box-shadow: none; } .dashboard-hero__backdrop { @@ -673,25 +1116,33 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { position: relative; display: flex; flex-wrap: wrap; - align-items: flex-start; + align-items: center; justify-content: space-between; - gap: 14px; - padding: 14px 18px; + gap: 12px; + min-height: 46px; + padding: 16px 16px 10px; } .dashboard-hero__primary { display: flex; - flex: 1 1 720px; - align-items: flex-start; - justify-content: space-between; - gap: 18px; + flex: 1 1 auto; + align-items: center; + justify-content: flex-start; + gap: 12px; } .dashboard-hero__heading { display: flex; - flex-direction: column; - gap: 8px; - max-width: 760px; + flex-direction: row; + align-items: baseline; + gap: 12px; + min-width: 0; +} + +.dashboard-hero__copy { + display: flex; + align-items: baseline; + gap: 12px; min-width: 0; } @@ -718,19 +1169,22 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { } .dashboard-title { - font-size: clamp(22px, 2.8vw, 32px); - line-height: 1; - letter-spacing: -0.05em; - font-weight: 600; + font-family: var(--font-mono); + font-size: 13px; + line-height: 1.1; + letter-spacing: -0.02em; + font-weight: 500; + white-space: nowrap; color: var(--color-text-primary); } .dashboard-subtitle { - margin-top: 3px; - max-width: 56ch; + margin-top: 0; + max-width: none; font-size: 11px; line-height: 1.4; color: var(--color-text-muted); + white-space: nowrap; } .dashboard-hero__meta { @@ -738,10 +1192,113 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { flex: 0 0 auto; flex-direction: row; align-items: center; - gap: 10px; + gap: 12px; margin-left: auto; } +.dashboard-topbar-meta { + display: flex; + align-items: center; + gap: 12px; + font-family: var(--font-mono); + font-size: 11px; + color: var(--color-text-muted); + white-space: nowrap; +} + +.dashboard-topbar-summary { + color: var(--color-text-muted); +} + +.dashboard-topbar-link { + display: inline-flex; + align-items: center; + gap: 7px; + color: var(--color-text-muted); + text-decoration: none; + transition: + color 120ms ease-out, + border-color 120ms ease-out, + background-color 120ms ease-out, + box-shadow 120ms ease-out, + transform 120ms cubic-bezier(0.23, 1, 0.32, 1); +} + +.dashboard-topbar-link:hover { + color: var(--color-text-primary); + text-decoration: none; +} + +.dashboard-topbar-link--orchestrator { + position: relative; + gap: 9px; + border: 1px solid color-mix(in srgb, var(--color-accent) 30%, transparent); + background: linear-gradient( + 180deg, + color-mix(in srgb, white 14%, var(--color-accent-hover)) 0%, + var(--color-accent) 100% + ); + padding: 6px 12px 6px 8px; + color: rgba(255, 255, 255, 0.96); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.28), + 0 1px 0 color-mix(in srgb, var(--color-accent) 28%, transparent), + 0 8px 18px color-mix(in srgb, var(--color-accent) 18%, transparent); + text-shadow: 0 1px 0 rgba(0, 0, 0, 0.12); +} + +@media (hover: hover) and (pointer: fine) { + .dashboard-topbar-link--orchestrator:hover { + color: #ffffff; + border-color: color-mix(in srgb, var(--color-accent) 42%, transparent); + background: linear-gradient( + 180deg, + color-mix(in srgb, white 18%, var(--color-accent-hover)) 0%, + color-mix(in srgb, var(--color-accent) 92%, black) 100% + ); + transform: translateY(-1px); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.32), + 0 1px 0 color-mix(in srgb, var(--color-accent) 30%, transparent), + 0 10px 20px color-mix(in srgb, var(--color-accent) 22%, transparent); + } +} + +.dashboard-topbar-link--orchestrator:active { + transform: scale(0.98); +} + +.dashboard-topbar-link__chip { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 34px; + padding: 3px 6px; + border: 1px solid rgba(255, 255, 255, 0.2); + background: rgba(255, 255, 255, 0.14); + color: rgba(255, 255, 255, 0.92); + font-size: 9px; + line-height: 1; + letter-spacing: 0.12em; + text-transform: uppercase; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.16); +} + +.dashboard-topbar-link__label { + color: inherit; + font-size: 11px; + line-height: 1.1; + letter-spacing: 0.05em; + white-space: nowrap; +} + +.dashboard-topbar-link__arrow { + color: rgba(255, 255, 255, 0.88); + font-size: 12px; + line-height: 1; + transform: translateY(-0.5px); +} + .dashboard-stat-cards { display: flex; flex-wrap: wrap; @@ -761,8 +1318,8 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { gap: 6px; border: 1px solid color-mix(in srgb, var(--color-border-default) 88%, transparent); border-radius: 0; - background: color-mix(in srgb, var(--color-bg-base) 72%, transparent); - padding: 6px 9px; + background: color-mix(in srgb, var(--color-bg-surface) 65%, transparent); + padding: 5px 8px; box-shadow: none; } @@ -778,6 +1335,208 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { font-variant-numeric: tabular-nums; } +.dashboard-stat-card__value--working { + color: var(--color-status-working); +} + +.dashboard-stat-card__value--review { + color: var(--color-accent-orange); +} + +/* ── Project metric tone colors ──────────────────────────────────────── */ + +.project-metric__value[data-tone="ready"] { + color: var(--color-status-ready); +} +.project-metric__value[data-tone="error"] { + color: var(--color-status-error); +} +.project-metric__value[data-tone="orange"] { + color: var(--color-accent-orange); +} +.project-metric__value[data-tone="attention"] { + color: var(--color-status-attention); +} +.project-metric__value[data-tone="working"] { + color: var(--color-status-working); +} + +/* ── Board legend dot colors ─────────────────────────────────────────── */ + +.board-legend-item__dot[data-tone="error"] { + background: var(--color-status-error); +} +.board-legend-item__dot[data-tone="orange"] { + background: var(--color-accent-orange); +} +.board-legend-item__dot[data-tone="ready"] { + background: var(--color-status-ready); +} + +/* ── Sidebar metric value colors ─────────────────────────────────────── */ + +.project-sidebar__metric-value--attention { + color: var(--color-status-attention); +} +.project-sidebar__metric-value--error { + color: var(--color-status-error); +} + +/* ── Sidebar session dot colors ──────────────────────────────────────── */ + +.sidebar-session-dot { + width: 6px; + height: 6px; +} +.sidebar-session-dot--glow { + box-shadow: 0 0 4px rgba(34, 197, 94, 0.5); +} +.dark .sidebar-session-dot--glow { + box-shadow: 0 0 5px rgba(34, 197, 94, 0.6); +} +.sidebar-session-dot[data-level="working"] { + background: var(--color-status-working); +} +.sidebar-session-dot[data-level="pending"] { + background: var(--color-status-attention); +} +.sidebar-session-dot[data-level="review"] { + background: var(--color-accent-orange); +} +.sidebar-session-dot[data-level="respond"] { + background: var(--color-status-respond); +} +.sidebar-session-dot[data-level="merge"] { + background: var(--color-status-ready); +} +.sidebar-session-dot[data-level="done"] { + background: var(--color-status-done); +} + +/* ── Sidebar health dot colors ───────────────────────────────────────── */ + +.sidebar-health-dot[data-health="red"], +.project-sidebar__health-indicator[data-health="red"] { + background: var(--color-status-error); +} +.sidebar-health-dot[data-health="yellow"], +.project-sidebar__health-indicator[data-health="yellow"] { + background: var(--color-status-attention); +} +.sidebar-health-dot[data-health="amber"], +.project-sidebar__health-indicator[data-health="amber"] { + background: var(--color-status-respond); +} +.sidebar-health-dot[data-health="green"], +.project-sidebar__health-indicator[data-health="green"] { + background: var(--color-status-ready); +} +.sidebar-health-dot[data-health="gray"], +.project-sidebar__health-indicator[data-health="gray"] { + background: var(--color-text-tertiary); +} +.sidebar-health-dot[data-health="done"], +.project-sidebar__health-indicator[data-health="done"] { + background: var(--color-status-done); +} + +/* ── Kanban / accordion / mobile dot colors (by level) ───────────────── */ + +.kanban-column__dot[data-level="working"] { + background: var(--color-status-working); +} +.kanban-column__dot[data-level="pending"] { + background: var(--color-status-attention); +} +.kanban-column__dot[data-level="review"] { + background: var(--color-accent-orange); +} +.kanban-column__dot[data-level="respond"] { + background: var(--color-status-respond); +} +.kanban-column__dot[data-level="merge"] { + background: var(--color-status-ready); +} +.kanban-column__dot[data-level="done"] { + background: var(--color-text-tertiary); +} + +/* ── Activity dot / pill colors ──────────────────────────────────────── */ + +/* Base fallbacks for null / unknown activity states */ +.activity-dot { + background: var(--color-text-tertiary); +} +.activity-pill { + background: var(--color-tint-neutral); +} +.activity-pill__text { + color: var(--color-text-muted); +} + +.activity-dot[data-activity="active"] { + background: var(--color-status-working); +} +.activity-dot[data-activity="ready"] { + background: var(--color-status-ready); +} +.activity-dot[data-activity="idle"] { + background: var(--color-status-idle); +} +.activity-dot[data-activity="waiting_input"] { + background: var(--color-status-attention); +} +.activity-dot[data-activity="blocked"] { + background: var(--color-status-error); +} +.activity-dot[data-activity="exited"] { + background: var(--color-status-done); +} + +.activity-pill[data-activity="active"] { + background: var(--color-tint-green); +} +.activity-pill[data-activity="ready"] { + background: var(--color-tint-blue); +} +.activity-pill[data-activity="idle"] { + background: var(--color-tint-neutral); +} +.activity-pill[data-activity="waiting_input"] { + background: var(--color-tint-yellow); +} +.activity-pill[data-activity="blocked"] { + background: var(--color-tint-red); +} +.activity-pill[data-activity="exited"] { + background: var(--color-tint-neutral); +} + +.activity-pill__text[data-activity="active"] { + color: var(--color-status-working); +} +.activity-pill__text[data-activity="ready"] { + color: var(--color-status-ready); +} +.activity-pill__text[data-activity="idle"] { + color: var(--color-text-secondary); +} +.activity-pill__text[data-activity="waiting_input"] { + color: var(--color-status-attention); +} +.activity-pill__text[data-activity="blocked"] { + color: var(--color-status-error); +} +.activity-pill__text[data-activity="exited"] { + color: var(--color-text-muted); +} + +/* ── Done card title color ───────────────────────────────────────────── */ + +.session-card-done__title { + color: var(--done-title-color); +} + .dashboard-stat-card__label { font-size: 10px; font-weight: 700; @@ -792,7 +1551,7 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { } .dashboard-alert { - border-radius: 2px; + border-radius: 0; box-shadow: var(--detail-card-shadow); } @@ -801,7 +1560,7 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { .detail-card { background: var(--detail-card-bg); box-shadow: var(--detail-card-shadow); - border-radius: 2px; + border-radius: 0; } .session-detail-page { @@ -822,7 +1581,7 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { padding: 14px 16px; background: var(--detail-card-bg); box-shadow: var(--detail-card-shadow); - border-radius: 2px; + border-radius: 0; } @media (min-width: 900px) { @@ -884,32 +1643,122 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { } } +.session-detail-layout { + max-width: 1200px; + padding: 16px 24px 48px; +} + +.session-detail-top-strip { + margin-bottom: 18px; +} + +.session-detail-crumbs { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 14px; +} + +.session-detail-crumb-back { + display: inline-flex; + align-items: center; + gap: 3px; + font-size: 11px; + font-weight: 500; + color: var(--color-text-muted); + text-decoration: none; + transition: color 100ms; +} + +.session-detail-crumb-back:hover { + color: var(--color-text-primary); + text-decoration: none; +} + +.session-detail-crumb-sep { + color: var(--color-text-muted); + font-size: 10px; +} + +.session-detail-crumb-id { + font-family: var(--font-mono); + font-size: 10.5px; + color: var(--color-text-muted); +} + +.session-detail-identity { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 14px; +} + +.session-detail-identity__info { + flex: 1; + min-width: 0; +} + +.session-detail-identity__title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 16px; + font-weight: 600; + line-height: 1.35; + letter-spacing: -0.03em; + color: var(--color-text-primary); +} + +.session-detail-identity__pills { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + margin-top: 8px; +} + +.session-detail-identity__actions { + display: flex; + flex-shrink: 0; + gap: 6px; + padding-top: 2px; +} + +.session-detail-identity__actions--custom { + flex-wrap: wrap; + align-items: center; +} + +.session-detail-mode-badge { + display: inline-flex; + align-items: center; + padding: 2px 8px; + border: 1px solid var(--color-accent-amber-border); + background: var(--color-accent-amber-dim); + color: var(--color-accent-amber); + font-size: 10px; + font-weight: 600; + letter-spacing: 0.05em; +} + .session-detail-link-pill { display: inline-flex; align-items: center; - min-height: 28px; - padding: 0 10px; + gap: 4px; + min-height: 24px; + padding: 3px 9px; border: 1px solid var(--color-border-subtle); - border-radius: 2px; - background: color-mix(in srgb, var(--color-bg-elevated) 88%, transparent); + border-radius: 4px; + background: var(--color-bg-subtle); color: var(--color-text-secondary); - font-size: 11px; + font-size: 10.5px; font-weight: 500; -} - -.session-detail-link-pill { - transition: - border-color 0.14s ease, - color 0.14s ease, - background 0.14s ease; -} - -.session-detail-link-pill--link { - color: var(--color-accent-blue); - border-color: color-mix(in srgb, var(--color-accent-blue) 22%, var(--color-border-subtle)); - background: color-mix(in srgb, var(--color-accent-blue) 9%, transparent); + line-height: 1; text-decoration: none; - box-shadow: inset 0 1px 0 color-mix(in srgb, white 16%, transparent); + transition: + border-color 0.12s ease, + color 0.12s ease, + background 0.12s ease; } .session-detail-link-pill:hover { @@ -917,34 +1766,54 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { color: var(--color-text-primary); } -.session-detail-link-pill--link:hover { - border-color: color-mix(in srgb, var(--color-accent-blue) 44%, var(--color-border-strong)); +.session-detail-link-pill--branch { + font-family: var(--font-mono); + font-size: 10px; +} + +.session-detail-link-pill--branch-link { color: var(--color-accent-blue); - background: color-mix(in srgb, var(--color-accent-blue) 13%, transparent); + border-color: color-mix(in srgb, var(--color-accent-blue) 18%, var(--color-border-subtle)); + background: color-mix(in srgb, var(--color-accent-blue) 6%, transparent); } -.session-detail-link-pill--accent { - color: var(--color-accent); - border-color: color-mix(in srgb, var(--color-accent) 20%, var(--color-border-subtle)); - background: color-mix(in srgb, var(--color-accent) 8%, transparent); +.session-detail-link-pill--branch-link:hover { + color: var(--color-accent-blue); + border-color: color-mix(in srgb, var(--color-accent-blue) 36%, var(--color-border-strong)); + background: color-mix(in srgb, var(--color-accent-blue) 10%, transparent); } -.session-detail-link-pill--link.session-detail-link-pill--accent { - color: var(--color-accent); - border-color: color-mix(in srgb, var(--color-accent) 28%, var(--color-border-subtle)); - background: color-mix(in srgb, var(--color-accent) 10%, transparent); +.session-detail-link-pill--pr { + font-family: var(--font-mono); + font-size: 10.5px; + font-weight: 600; + color: var(--color-accent-amber); + border-color: var(--color-accent-amber-border); + background: var(--color-accent-amber-dim); } -.session-detail-link-pill--link.session-detail-link-pill--accent:hover { - border-color: color-mix(in srgb, var(--color-accent) 46%, var(--color-border-strong)); - color: var(--color-accent); - background: color-mix(in srgb, var(--color-accent) 14%, transparent); +.session-detail-link-pill--pr:hover { + color: var(--color-accent-amber); + border-color: var(--color-accent-amber-border); + background: rgba(249, 115, 22, 0.18); +} + +.session-detail-link-pill--diff { + font-family: var(--font-mono); + font-size: 10px; + gap: 3px; + padding: 2px 7px; } .session-detail-status-pill { - position: relative; - overflow: hidden; - box-shadow: inset 0 1px 0 color-mix(in srgb, white 16%, transparent); + display: inline-flex; + align-items: center; + gap: 5px; + padding: 3px 9px; + border: 1px solid; + border-radius: 4px; + font-size: 10.5px; + font-weight: 600; transition: transform 0.16s ease-out, opacity 0.16s ease-out, @@ -952,37 +1821,69 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { border-color 0.16s ease-out; } +.session-detail-status-pill__label { + color: inherit; +} + .session-detail-status-pill__dot { - position: relative; - z-index: 1; + width: 6px; + height: 6px; + border-radius: 999px; + flex-shrink: 0; } .session-detail-status-pill--active { - animation: session-status-active-breathe 2.8s ease-in-out infinite; + color: var(--color-status-working); + border-color: color-mix(in srgb, var(--color-status-working) 20%, transparent); + background: color-mix(in srgb, var(--color-status-working) 8%, transparent); +} + +.session-detail-status-pill--ready { + color: var(--color-status-merge); + border-color: color-mix(in srgb, var(--color-status-merge) 20%, transparent); + background: color-mix(in srgb, var(--color-status-merge) 8%, transparent); +} + +.session-detail-status-pill--waiting { + color: var(--color-status-respond); + border-color: color-mix(in srgb, var(--color-status-respond) 20%, transparent); + background: color-mix(in srgb, var(--color-status-respond) 8%, transparent); +} + +.session-detail-status-pill--error { + color: var(--color-status-error, var(--color-accent-red)); + border-color: color-mix(in srgb, var(--color-status-error, var(--color-accent-red)) 20%, transparent); + background: color-mix(in srgb, var(--color-status-error, var(--color-accent-red)) 8%, transparent); +} + +.session-detail-status-pill--neutral { + color: var(--color-text-muted); + border-color: color-mix(in srgb, var(--color-text-muted) 16%, transparent); + background: color-mix(in srgb, var(--color-text-muted) 6%, transparent); } .session-detail-status-pill--active .session-detail-status-pill__dot { animation: session-status-dot-pulse 1.8s ease-in-out infinite; } -.session-detail-status-pill--ready { - animation: session-status-ready-breathe 3.2s ease-in-out infinite; -} - .session-detail-status-pill--ready .session-detail-status-pill__dot { animation: session-status-dot-pulse 2.2s ease-in-out infinite; } .session-detail-status-pill--idle { + color: var(--color-text-muted); + border-color: color-mix(in srgb, var(--color-text-muted) 16%, transparent); + background: color-mix(in srgb, var(--color-text-muted) 6%, transparent); opacity: 0.92; } -.session-detail-status-pill--waiting { - animation: session-status-waiting-breathe 3s ease-in-out infinite; +.session-detail-status-pill--waiting .session-detail-status-pill__dot { + animation: session-status-dot-pulse 2s ease-in-out infinite; } @keyframes session-status-dot-pulse { - 0%, 100% { + 0%, + 100% { transform: scale(1); opacity: 0.88; } @@ -992,65 +1893,81 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { } } -@keyframes session-status-active-breathe { - 0%, 100% { - transform: translateY(0); - opacity: 0.96; - } - 50% { - transform: translateY(-1px); - opacity: 1; - } +.session-detail-pr-section { + margin-bottom: 16px; } -@keyframes session-status-ready-breathe { - 0%, 100% { - transform: translateY(0); - opacity: 0.96; - } - 50% { - transform: translateY(-1px); - opacity: 1; - } +.session-detail-terminal-wrap { + margin-bottom: 18px; } -@keyframes session-status-waiting-breathe { - 0%, 100% { - transform: translateY(0); - opacity: 0.95; - } - 50% { - transform: translateY(-1px); - opacity: 1; - } +.session-detail-section-label { + display: flex; + align-items: center; + gap: 7px; + margin-bottom: 8px; } -.dark .detail-card { - --color-text-secondary: #9898a0; - --color-text-muted: #5c5c66; - --color-text-tertiary: #5c5c66; +.session-detail-section-label__bar { + width: 2px; + height: 11px; + border-radius: 1px; } -/* ── Session cards — flat, Linear-style ──────────────────────────────── */ +.session-detail-section-label__text { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-text-muted); +} + +.session-detail-terminal-placeholder { + border: 1px solid var(--color-border-default); + border-radius: 6px; + background: color-mix(in srgb, var(--color-bg-elevated) 92%, transparent); +} + +/* ── Session cards — Emil design vision ──────────────────────────────── */ .session-card { background: var(--card-bg); - border-color: var(--card-border); - border-radius: 2px; - box-shadow: - inset 0 1px 0 color-mix(in srgb, white 4%, transparent), - 0 10px 20px rgba(0, 0, 0, 0.08); + border: 1px solid var(--card-border); + border-left-width: 3px; + border-left-style: solid; + border-radius: 6px; + box-shadow: none; + color: color-mix(in srgb, var(--color-text-primary) 96%, transparent); contain: layout style paint; transition: - transform 0.12s ease, - border-color 0.12s ease, - background 0.12s ease, - box-shadow 0.12s ease; + transform 120ms ease-out, + border-color 120ms ease-out, + box-shadow 120ms ease-out; +} + +.session-card--accent-working { + border-left-color: var(--color-status-working); +} + +.session-card--accent-attention { + border-left-color: var(--color-status-attention); +} + +.session-card--accent-respond { + border-left-color: var(--color-status-respond); +} + +.session-card--accent-merge { + border-left-color: var(--color-status-ready); +} + +.session-card--accent-default { + border-left-color: var(--color-border-default); } .session-card--fixed { display: flex; - min-height: 242px; + min-height: 150px; flex-direction: column; overflow: hidden; } @@ -1064,20 +1981,15 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { } .session-card--alert-frame { - min-height: 292px; + min-height: 174px; } .session-card--merge-frame { - min-height: 264px; + min-height: 166px; } .dark .session-card { - --color-text-secondary: #9898a0; - --color-text-muted: #5c5c66; - --color-text-tertiary: #5c5c66; - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.05), - 0 14px 30px rgba(0, 0, 0, 0.34); + box-shadow: none; } /* ── Dark mode weight reduction ──────────────────────────────────────── @@ -1100,25 +2012,304 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { .session-card:hover { transform: translateY(-1px); border-color: var(--color-border-strong); - box-shadow: - inset 0 1px 0 color-mix(in srgb, white 5%, transparent), - 0 18px 34px rgba(0, 0, 0, 0.09); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); +} + +.session-card:active { + transform: scale(0.985); + transition-duration: 100ms; } .session-card__header, -.session-card__title-wrap, -.session-card__meta, +.session-card__body, +.card__title-wrap, +.card__meta, +.card__alerts, .session-card__actions { position: relative; z-index: 1; } -.session-card__title-wrap::after { - display: none; +.session-card__header { + display: flex; + align-items: center; + gap: 6px; + padding: 7px 10px 3px; } -.session-card__title { +.card__id { + font-family: var(--font-mono); + font-size: 9.5px; + letter-spacing: 0.04em; + color: var(--color-text-muted); + flex: 1; +} + +.card__adot { + position: relative; + width: 7px; + height: 7px; + flex-shrink: 0; + border-radius: 999px; +} + +.card__adot--working { + background: var(--color-status-working); +} + +@keyframes design-ref-adot-pulse { + 0%, + 100% { + opacity: 0.42; + transform: scale(1); + } + 50% { + opacity: 0; + transform: scale(1.55); + } +} + +.card__adot--working::after { + content: ""; + position: absolute; + inset: -2px; + border: 1px solid color-mix(in srgb, var(--color-status-working) 42%, transparent); + border-radius: inherit; + animation: design-ref-adot-pulse 2s ease-in-out infinite; +} + +.card__adot--ready { + background: var(--color-status-ready); + opacity: 0.5; +} + +.card__adot--idle { + background: var(--color-text-muted); +} + +.card__adot--waiting { + background: var(--color-status-respond); +} + +.card__adot--blocked { + background: var(--color-ci-fail); +} + +.card__adot--exited { + background: var(--color-status-done); +} + +.card__title-wrap { + padding: 0 10px 4px; +} + +.card__title { + font-size: 12.5px; + font-weight: 600; + line-height: 1.45; + color: var(--color-text-primary); + display: -webkit-box; + -webkit-box-orient: vertical; -webkit-line-clamp: 2; + overflow: hidden; +} + +.card__title--dim { + font-weight: 500; + color: var(--color-text-secondary); +} + +.card__meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 4px; + padding: 0 10px 5px; +} + +.card__branch { + font-family: var(--font-mono); + font-size: 9.5px; + color: var(--color-text-muted); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 104px; +} + +.card__meta-sep { + color: var(--color-text-muted); + font-size: 10px; +} + +.card__pr { + font-family: var(--font-mono); + font-size: 10px; + font-weight: 600; + color: var(--color-text-secondary); + background: var(--color-bg-subtle); + border: 1px solid var(--color-border-default); + padding: 1px 5px; + border-radius: 3px; + text-decoration: none; + white-space: nowrap; + transition: color 80ms, border-color 80ms, background 80ms; +} + +.card__pr:hover { + color: var(--color-text-primary); + border-color: var(--color-border-strong); + background: var(--color-bg-elevated); + text-decoration: none; +} + +.card__diff { + background: var(--color-bg-subtle); + padding: 1px 5px; + border-radius: 3px; + font-family: var(--font-mono); + font-size: 9.5px; + gap: 2px; +} + +.card__diff-add { + color: var(--color-ci-pass); +} + +.card__diff-del { + color: var(--color-ci-fail); +} + +.card__diff-size { + color: var(--color-text-muted); +} + +.card__ci { + display: flex; + flex-wrap: wrap; + gap: 3px; + padding: 0 10px 5px; +} + +.ci-chip { + display: inline-flex; + align-items: center; + gap: 3px; + font-size: 9.5px; + font-family: var(--font-mono); + padding: 1px 6px; + border-radius: 3px; +} + +.ci-chip--pass { + color: var(--color-ci-pass); + border: 1px solid rgba(34, 197, 94, 0.2); + background: rgba(34, 197, 94, 0.06); +} + +.card__alerts { + padding: 4px 10px 6px; + display: flex; + flex-direction: column; + gap: 3px; + align-items: stretch; +} + +/* ── Alert rows ────────────────────────────────────────────────── */ + +.alert-row { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 8px 4px 7px; + border-radius: 4px; + font-size: 11px; + border-left: 2px solid var(--ar-color); + background: var(--ar-bg); + color: var(--ar-color); + transition: background 80ms; +} + +.alert-row--ci { --ar-color: var(--color-alert-ci); --ar-bg: var(--color-alert-ci-bg); } +.alert-row--changes { --ar-color: var(--color-alert-changes); --ar-bg: var(--color-alert-changes-bg); } +.alert-row--review { --ar-color: var(--color-alert-review); --ar-bg: var(--color-alert-review-bg); } +.alert-row--conflict { --ar-color: var(--color-alert-conflict); --ar-bg: var(--color-alert-conflict-bg); } +.alert-row--comment { --ar-color: var(--color-alert-comment); --ar-bg: var(--color-alert-comment-bg); } + +.alert-row__icon { + flex-shrink: 0; + display: flex; + align-items: center; +} + +.alert-row__text { + flex: 1; + min-width: 0; + font-size: 11px; + font-weight: 400; + color: var(--color-text-secondary); + min-width: 0; +} + +.alert-row__text a { + color: inherit; + text-decoration: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: block; +} + +.alert-row__text a:hover { + color: var(--color-text-primary); + text-decoration: underline; + text-underline-offset: 2px; +} + +.alert-row__notified { + opacity: 0.65; + font-weight: 400; +} + +.alert-row__action { + flex-shrink: 0; + padding: 2px 7px; + border: 1px solid var(--ar-color); + border-radius: 3px; + background: transparent; + color: var(--ar-color); + font-size: 9.5px; + font-weight: 600; + opacity: 0.85; + transition: background 100ms, color 100ms; + cursor: pointer; + white-space: nowrap; + text-transform: lowercase; + letter-spacing: 0.03em; +} + +.alert-row__action:hover { + background: var(--ar-color); + color: #fff; + opacity: 1; +} + +.alert-row__action:active { + transform: scale(0.96); +} + +.alert-row__action:disabled { + opacity: 0.4; + cursor: default; +} + +.card__issue { + font-size: 11px; + color: var(--color-accent); + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .session-card__secondary { @@ -1127,6 +2318,20 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-height: 1.45; + font-size: 10.5px; + color: var(--color-text-muted); +} + +.session-card__secondary--merge { + display: flex; + align-items: center; + gap: 4px; + color: var(--color-text-muted); +} + +.session-card__secondary--merge svg { + flex-shrink: 0; + color: var(--color-ci-pass); } .session-card__alert-grid { @@ -1141,18 +2346,47 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { .session-card__footer { margin-top: auto; - min-height: 34px; - background: color-mix(in srgb, var(--color-bg-base) 35%, transparent); + min-height: 0; + display: flex; + align-items: center; + gap: 6px; + border-top: 1px solid var(--color-border-subtle); + background: transparent; + padding: 5px 10px 7px; +} + +.card__status { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 10.5px; + line-height: 1.2; + font-family: var(--font-mono); + font-weight: 400; + color: var(--color-text-muted); } .session-card__control { - height: 28px; - padding-left: 10px; - padding-right: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + text-decoration: none; + transition: + transform 160ms ease-out, + border-color 120ms ease-out, + color 120ms ease-out; + height: 26px; box-sizing: border-box; white-space: nowrap; } +.session-card__control:active { + transform: scale(0.97); +} + .session-card__control-icon { width: 12px; height: 12px; @@ -1160,40 +2394,91 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { } .session-card__terminate { - border-radius: 0; line-height: 1; - padding-left: 8px; - padding-right: 8px; +} + +.session-card__terminal-link { + font-size: 10.5px; + font-family: var(--font-sans); + font-weight: 500; + padding: 2px 8px 2px 6px; + border-radius: 4px; + border: 1px solid var(--color-border-default); + background: var(--color-bg-elevated); + color: var(--color-text-secondary); +} + +.session-card__terminal-link:hover { + background: var(--color-accent-amber-dim); + border-color: var(--color-accent-amber-border); + color: var(--color-accent-amber); + text-decoration: none; +} + +.btn--danger { + width: 26px; + height: 26px; + padding: 0; + border-radius: 4px; + border: 1px solid rgba(239, 68, 68, 0.2); + background: rgba(239, 68, 68, 0.04); + color: rgba(239, 68, 68, 0.45); +} + +.btn--danger:hover { + background: rgba(239, 68, 68, 0.1); + border-color: rgba(239, 68, 68, 0.4); + color: var(--color-ci-fail); +} + +.btn--danger.is-confirming { + width: auto; + min-width: 44px; + padding: 0 8px; + background: rgba(239, 68, 68, 0.14); + border-color: rgba(239, 68, 68, 0.6); + color: var(--color-ci-fail); + animation: kill-confirm-pulse 0.6s cubic-bezier(0.23, 1, 0.32, 1); +} + +@keyframes kill-confirm-pulse { + 0% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(239, 68, 68, 0.22); + } + 55% { + transform: scale(1.04); + box-shadow: 0 0 0 6px rgba(239, 68, 68, 0); + } + 100% { + transform: scale(1); + box-shadow: 0 0 0 0 rgba(239, 68, 68, 0); + } +} + +.btn--danger.is-confirming::after { + content: none; } .session-card__merge-control { - border-color: color-mix(in srgb, var(--color-status-ready) 68%, transparent); - background: linear-gradient( - 180deg, - color-mix(in srgb, var(--color-status-ready) 34%, transparent) 0%, - color-mix(in srgb, var(--color-status-ready) 18%, transparent) 100% - ); - color: var(--color-status-ready); - text-decoration: none; + font-size: 11px; font-weight: 600; - letter-spacing: 0.01em; - box-shadow: - inset 0 1px 0 color-mix(in srgb, white 26%, transparent), - 0 8px 18px color-mix(in srgb, var(--color-status-ready) 16%, transparent); + padding: 3px 10px; + border-radius: 4px; + border: 1px solid var(--color-accent-amber-border); + background: var(--color-accent-amber-dim); + color: var(--color-accent-amber); + text-decoration: none; + box-shadow: none; + cursor: pointer; } .session-card__merge-control:hover { - border-color: var(--color-status-ready); - background: linear-gradient( - 180deg, - color-mix(in srgb, var(--color-status-ready) 46%, transparent) 0%, - color-mix(in srgb, var(--color-status-ready) 24%, transparent) 100% - ); + border-color: var(--color-accent-amber-border); + background: rgba(249, 115, 22, 0.2); text-decoration: none; - transform: translateY(-1px); - box-shadow: - inset 0 1px 0 color-mix(in srgb, white 30%, transparent), - 0 10px 22px color-mix(in srgb, var(--color-status-ready) 20%, transparent); + transform: none; + box-shadow: none; } .session-card__merge-control .session-card__control-icon { @@ -1206,110 +2491,20 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { .session-card.card-merge-ready { position: relative; - background: linear-gradient( - 180deg, - color-mix(in srgb, var(--color-status-ready) 10%, var(--card-merge-bg)) 0%, - color-mix(in srgb, var(--color-status-ready) 6%, var(--card-merge-bg)) 28%, - color-mix(in srgb, var(--color-status-ready) 3%, var(--card-merge-bg)) 100% - ); - border-color: color-mix(in srgb, var(--color-status-ready) 30%, var(--color-border-default)); - box-shadow: - inset 0 1px 0 color-mix(in srgb, white 4%, transparent), - 0 22px 40px color-mix(in srgb, var(--color-status-ready) 18%, transparent); + background: var(--card-bg); + border-color: var(--card-border); } -.session-card.card-merge-ready::before { - display: none; +.dark .session-card.card-merge-ready { + box-shadow: none; } -.session-card.card-merge-ready::after { - content: ""; - position: absolute; - inset: -14px -10px -18px -10px; - pointer-events: none; - z-index: 0; - background: - radial-gradient( - ellipse at 50% 100%, - color-mix(in srgb, var(--color-status-ready) 18%, transparent) 0%, - color-mix(in srgb, var(--color-status-ready) 8%, transparent) 42%, - transparent 72% - ), - radial-gradient( - ellipse at 50% 0%, - color-mix(in srgb, var(--color-status-ready) 10%, transparent) 0%, - transparent 58% - ); - filter: blur(10px); - opacity: 0.9; -} - -.session-card.card-merge-ready > * { - position: relative; - z-index: 1; -} - -.dark .session-card.card-merge-ready::after { - background: - radial-gradient( - ellipse at 50% 100%, - color-mix(in srgb, var(--color-status-ready) 24%, transparent) 0%, - color-mix(in srgb, var(--color-status-ready) 10%, transparent) 44%, - transparent 72% - ), - radial-gradient( - ellipse at 50% 0%, - color-mix(in srgb, var(--color-status-ready) 12%, transparent) 0%, - transparent 58% - ); - opacity: 0.75; -} - -.session-card.card-merge-ready:hover::after { - opacity: 1; - filter: blur(12px); - background: - radial-gradient( - ellipse at 50% 100%, - color-mix(in srgb, var(--color-status-ready) 24%, transparent) 0%, - color-mix(in srgb, var(--color-status-ready) 10%, transparent) 44%, - transparent 74% - ), - radial-gradient( - ellipse at 50% 0%, - color-mix(in srgb, var(--color-status-ready) 12%, transparent) 0%, - transparent 58% - ); -} - -.dark .session-card.card-merge-ready:hover::after { - background: - radial-gradient( - ellipse at 50% 100%, - color-mix(in srgb, var(--color-status-ready) 28%, transparent) 0%, - color-mix(in srgb, var(--color-status-ready) 12%, transparent) 44%, - transparent 74% - ), - radial-gradient( - ellipse at 50% 0%, - color-mix(in srgb, var(--color-status-ready) 14%, transparent) 0%, - transparent 58% - ); -} - -.session-card.card-merge-ready::after { - transition: - opacity 0.18s ease, - filter 0.18s ease, - background 0.18s ease; -} +/* Merge card glow removed for cleaner Emil design */ .session-card.card-merge-ready:hover { - transform: translateY(-2px); - border-color: color-mix(in srgb, var(--color-status-ready) 42%, var(--color-border-strong)); - box-shadow: - inset 0 1px 0 color-mix(in srgb, white 6%, transparent), - 0 28px 52px color-mix(in srgb, var(--color-status-ready) 22%, transparent); + transform: translateY(-1px); + border-color: var(--color-border-strong); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12); } .session-card.card-merge-ready .session-card__header > span:first-child > span:first-child { @@ -1319,89 +2514,101 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { /* ── Quick-reply section (respond cards) ─────────────────────────────── */ .quick-reply { - display: flex; - flex-direction: column; - gap: 8px; - padding: 10px 12px; - border-top: 1px solid var(--color-border-default); + padding: 0 10px 5px; } -.quick-reply__summary { - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; - overflow: hidden; - font-size: 12px; - line-height: 1.5; - color: var(--color-text-secondary); - margin: 0; -} - -.quick-reply__presets { +.card__agent-msg { display: flex; gap: 6px; + padding: 0 0 4px; + font-size: 10px; + color: var(--color-text-secondary); + line-height: 1.45; } -.quick-reply__preset-btn { - min-height: 44px; - padding: 0 14px; +.card__agent-msg-icon { + color: var(--color-status-respond); + flex-shrink: 0; + margin-top: 1px; +} + +.card__view-context { + display: block; + font-size: 9.75px; + color: var(--color-text-muted); + padding: 0 0 6px; + text-decoration: none; + transition: color 100ms; +} + +.card__view-context:hover { + color: var(--color-text-secondary); + text-decoration: none; +} + +.card__presets { + display: flex; + gap: 3px; + margin-bottom: 5px; +} + +.card__preset { + flex: 1; + padding: 3px 0; + border-radius: 4px; border: 1px solid var(--color-border-default); - background: var(--color-bg-subtle); + background: transparent; color: var(--color-text-secondary); font-family: var(--font-sans); - font-size: 12px; + font-size: 10.5px; + font-weight: 500; cursor: pointer; - transition: border-color 0.12s ease, color 0.12s ease, background 0.12s ease; + text-align: center; + transition: + transform 160ms ease-out, + border-color 120ms ease-out; } -.quick-reply__preset-btn:hover { - border-color: var(--color-accent); - color: var(--color-accent); - background: var(--color-tint-blue); -} - -.quick-reply__input { - min-height: 44px; - height: 44px; - padding: 12px; - border: 1px solid var(--color-border-default); +.card__preset:hover { background: var(--color-bg-subtle); + border-color: var(--color-border-strong); color: var(--color-text-primary); - font-family: var(--font-sans); - font-size: 13px; - resize: none; - overflow: hidden; - transition: height 0.15s ease, border-color 0.12s ease; - box-sizing: border-box; +} + +.card__preset:active { + transform: scale(0.96); +} + +.card__reply-wrap { + padding: 0; +} + +.card__reply { width: 100%; -} - -.quick-reply__input::placeholder { - color: var(--color-text-muted); -} - -.quick-reply__input:focus { + padding: 5px 8px; + font-family: var(--font-sans); + font-size: 10.75px; + background: var(--color-bg-subtle); + border: 1px solid var(--color-border-default); + border-radius: 4px; + color: var(--color-text-primary); + resize: none; outline: none; - border-color: var(--color-accent); - height: 80px; - overflow: auto; + transition: border-color 120ms ease-out; +} + +.card__reply::placeholder { + color: var(--color-text-muted); + font-size: 10.5px; +} + +.card__reply:focus { + border-color: var(--color-border-strong); } @media (min-width: 768px) { - .quick-reply__preset-btn { - min-height: 32px; - padding: 0 10px; - font-size: 11px; - } - - .quick-reply__input { - min-height: 32px; - height: 32px; - padding: 8px 10px; - } - - .quick-reply__input:focus { - height: 60px; + .card__preset { + font-size: 10px; } } @@ -1533,6 +2740,386 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { opacity: 0.6; } +/* ── Session detail redesign — compact PR card ─────────────────────── */ + +.session-detail-pr-card { + border: 1px solid var(--color-border-default); + border-radius: 6px; + background: var(--color-bg-card, var(--detail-card-bg, var(--color-bg-elevated))); + overflow: hidden; +} + +.session-detail-pr-card--green { + border-color: color-mix(in srgb, var(--color-accent-green) 35%, var(--color-border-default)); +} + +.session-detail-pr-card__row { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + min-height: 0; +} + +.session-detail-pr-card__title-link { + font-size: 12.5px; + font-weight: 600; + color: var(--color-text-primary); + text-decoration: none; + transition: color 120ms; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + min-width: 0; +} + +.session-detail-pr-card__title-link:hover { + color: var(--color-accent-amber); +} + +.session-detail-pr-card__diff-stats { + font-family: var(--font-mono); + font-size: 10px; + flex-shrink: 0; +} + +.session-detail-pr-card__diff-label { + font-size: 9.5px; + font-weight: 600; + padding: 1px 6px; + border-radius: 3px; + background: var(--color-bg-subtle); + border: 1px solid var(--color-border-subtle); + color: var(--color-text-muted); + flex-shrink: 0; +} + +/* PR detail strips — inline blockers + CI chips */ +.session-detail-pr-card__details { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 6px; + padding: 0 14px 10px; +} + +.session-detail-pr-sep { + width: 1px; + height: 14px; + background: var(--color-border-subtle); + flex-shrink: 0; +} + +/* Blocker chips */ +.session-detail-blocker-chip { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 10.5px; + padding: 2px 8px; + border-radius: 3px; +} + +.session-detail-blocker-chip--fail { + color: var(--color-status-error, var(--color-accent-red)); + background: color-mix(in srgb, var(--color-status-error, var(--color-accent-red)) 8%, transparent); + border: 1px solid color-mix(in srgb, var(--color-status-error, var(--color-accent-red)) 18%, transparent); +} + +.session-detail-blocker-chip--warn { + color: var(--color-status-attention, var(--color-accent-yellow)); + background: color-mix(in srgb, var(--color-status-attention, var(--color-accent-yellow)) 8%, transparent); + border: 1px solid color-mix(in srgb, var(--color-status-attention, var(--color-accent-yellow)) 18%, transparent); +} + +.session-detail-blocker-chip--muted { + color: var(--color-text-muted); + background: var(--color-bg-subtle); + border: 1px solid var(--color-border-subtle); +} + +.session-detail-blocker-chip__note { + font-size: 9px; + opacity: 0.7; +} + +/* CI inline chips */ +.session-detail-ci-chip { + display: inline-flex; + align-items: center; + gap: 3px; + font-size: 10px; + font-family: var(--font-mono); + padding: 2px 7px; + border-radius: 3px; +} + +.session-detail-ci-chip--pass { + color: var(--color-accent-green); + border: 1px solid color-mix(in srgb, var(--color-accent-green) 20%, transparent); + background: color-mix(in srgb, var(--color-accent-green) 6%, transparent); +} + +.session-detail-ci-chip--fail { + color: var(--color-accent-red, var(--color-status-error)); + border: 1px solid color-mix(in srgb, var(--color-accent-red, var(--color-status-error)) 20%, transparent); + background: color-mix(in srgb, var(--color-accent-red, var(--color-status-error)) 6%, transparent); +} + +.session-detail-ci-chip--pending { + color: var(--color-accent-yellow, var(--color-status-attention)); + border: 1px solid color-mix(in srgb, var(--color-accent-yellow, var(--color-status-attention)) 20%, transparent); + background: color-mix(in srgb, var(--color-accent-yellow, var(--color-status-attention)) 6%, transparent); +} + +.session-detail-ci-chip--queued { + color: var(--color-text-muted); + border: 1px solid var(--color-border-subtle); + background: transparent; +} + +/* Merge-ready banner (compact, inline) */ +.session-detail-merge-banner { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 2px 10px; + border-radius: 4px; + border: 1px solid color-mix(in srgb, var(--color-accent-green) 25%, transparent); + background: color-mix(in srgb, var(--color-accent-green) 8%, transparent); + font-size: 11px; + font-weight: 600; + color: var(--color-accent-green); +} + +/* Collapsible comments strip */ +.session-detail-comments-strip { + border-top: 1px solid var(--color-border-subtle); +} + +.session-detail-comments-strip summary { + list-style: none; + cursor: pointer; +} + +.session-detail-comments-strip summary::-webkit-details-marker { + display: none; +} + +.session-detail-comments-strip__toggle { + display: flex; + align-items: center; + gap: 7px; + padding: 8px 14px; + font-size: 11px; + transition: background 80ms; +} + +.session-detail-comments-strip__toggle:hover { + background: var(--color-bg-subtle); +} + +.session-detail-comments-strip__chevron { + width: 10px; + height: 10px; + flex-shrink: 0; + color: var(--color-text-muted); + transition: transform 150ms; +} + +.session-detail-comments-strip[open] .session-detail-comments-strip__chevron { + transform: rotate(90deg); +} + +.session-detail-comments-strip__label { + font-weight: 600; + color: var(--color-text-secondary); +} + +.session-detail-comments-strip__count { + font-size: 10px; + font-weight: 700; + padding: 1px 6px; + border-radius: 3px; + color: var(--color-accent-red, var(--color-status-error)); + background: color-mix(in srgb, var(--color-accent-red, var(--color-status-error)) 12%, transparent); +} + +.session-detail-comments-strip__hint { + margin-left: auto; + font-size: 10px; + color: var(--color-text-muted); +} + +.session-detail-comments-strip__body { + padding: 0 14px 10px; +} + +/* Individual comment row */ +.session-detail-comment__row { + display: flex; + align-items: center; + gap: 7px; + padding: 4px 8px; + font-size: 12px; + border-radius: 4px; + transition: background 80ms; +} + +.session-detail-comment__row:hover { + background: var(--color-bg-subtle); +} + +.session-detail-comment summary { + list-style: none; + cursor: pointer; +} + +.session-detail-comment summary::-webkit-details-marker { + display: none; +} + +.session-detail-comment__chevron { + width: 11px; + height: 11px; + flex-shrink: 0; + color: var(--color-text-muted); + transition: transform 150ms; +} + +.session-detail-comment[open] .session-detail-comment__chevron { + transform: rotate(90deg); +} + +.session-detail-comment__title { + font-size: 12px; + font-weight: 500; + color: var(--color-text-secondary); +} + +.session-detail-comment__author { + font-size: 10.5px; + color: var(--color-text-muted); +} + +.session-detail-comment__view { + margin-left: auto; + font-size: 10px; + color: var(--color-accent-blue, var(--color-accent)); + text-decoration: none; + flex-shrink: 0; +} + +.session-detail-comment__view:hover { + text-decoration: underline; +} + +.session-detail-comment__body { + margin-left: 18px; + padding: 2px 8px 10px; +} + +.session-detail-comment__file { + font-family: var(--font-mono); + font-size: 10px; + color: var(--color-text-muted); + margin-bottom: 4px; +} + +.session-detail-comment__text { + border-left: 2px solid var(--color-border-default); + padding-left: 10px; + font-size: 11.5px; + line-height: 1.55; + color: var(--color-text-secondary); + margin-bottom: 6px; +} + +.session-detail-comment__fix-btn { + padding: 3px 10px; + border-radius: 4px; + border: 1px solid var(--color-accent-amber-border, color-mix(in srgb, var(--color-accent) 30%, transparent)); + background: var(--color-accent-amber-dim, color-mix(in srgb, var(--color-accent) 12%, transparent)); + color: var(--color-accent-amber, var(--color-accent)); + font-family: var(--font-sans, inherit); + font-size: 10.5px; + font-weight: 600; + cursor: pointer; + transition: background 100ms, transform 100ms cubic-bezier(0.23, 1, 0.32, 1); +} + +.session-detail-comment__fix-btn:hover { + background: color-mix(in srgb, var(--color-accent-amber, var(--color-accent)) 18%, transparent); +} + +.session-detail-comment__fix-btn:active { + transform: scale(0.97); +} + +.session-detail-comment__fix-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.session-detail-comment__fix-btn--sent { + border-color: color-mix(in srgb, var(--color-accent-green) 30%, transparent); + background: color-mix(in srgb, var(--color-accent-green) 12%, transparent); + color: var(--color-accent-green); +} + +.session-detail-comment__fix-btn--error { + border-color: color-mix(in srgb, var(--color-accent-red) 30%, transparent); + background: color-mix(in srgb, var(--color-accent-red) 12%, transparent); + color: var(--color-accent-red); +} + +/* Identity strip action buttons */ +.session-detail-action-btn { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 5px 11px; + border-radius: 5px; + border: 1px solid var(--color-border-default); + background: transparent; + color: var(--color-text-secondary); + font-family: inherit; + font-size: 11.5px; + font-weight: 500; + cursor: pointer; + transition: background 100ms, border-color 100ms, color 100ms, transform 100ms cubic-bezier(0.23, 1, 0.32, 1); +} + +.session-detail-action-btn:hover { + background: var(--color-bg-subtle); + border-color: var(--color-border-strong); + color: var(--color-text-primary); +} + +.session-detail-action-btn:active { + transform: scale(0.97); +} + +.session-detail-action-btn--danger { + color: var(--color-accent-red, var(--color-status-error)); + border-color: color-mix(in srgb, var(--color-accent-red, var(--color-status-error)) 22%, var(--color-border-default)); +} + +.session-detail-action-btn--danger:hover { + background: color-mix(in srgb, var(--color-accent-red, var(--color-status-error)) 8%, transparent); + border-color: color-mix(in srgb, var(--color-accent-red, var(--color-status-error)) 40%, var(--color-border-default)); + color: var(--color-accent-red, var(--color-status-error)); +} + +/* Diff stat colors (for PR card and pills) */ +.session-detail-diff--add { + color: var(--color-accent-green); +} + +.session-detail-diff--del { + color: var(--color-accent-red, var(--color-status-error)); +} + /* ── Project sidebar ────────────────────────────────────────────────── */ .project-sidebar nav::-webkit-scrollbar { @@ -1543,311 +3130,287 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { } .project-sidebar nav::-webkit-scrollbar-thumb { background: var(--color-scrollbar); - border-radius: 2px; + border-radius: 0; } .project-sidebar { - border-right: 1px solid var(--color-border-subtle); - background: linear-gradient( - 180deg, - color-mix(in srgb, var(--color-bg-elevated) 92%, black 8%) 0%, - color-mix(in srgb, var(--color-bg-base) 90%, black 10%) 100% - ); + --sidebar-bg: #f0ede9; + --sidebar-border: rgba(0, 0, 0, 0.06); + --sidebar-row-hover: rgba(120, 100, 80, 0.05); + --sidebar-row-active: rgba(120, 100, 80, 0.09); + border-right: 1px solid var(--sidebar-border); + background: var(--sidebar-bg); + color: var(--color-text-primary); + overflow: hidden; + width: 224px; +} + +.dark .project-sidebar { + --sidebar-bg: #0e0d0c; + --sidebar-border: rgba(255, 240, 220, 0.07); + --sidebar-row-hover: rgba(255, 240, 220, 0.05); + --sidebar-row-active: rgba(255, 240, 220, 0.07); } .project-sidebar--collapsed { - align-items: center; + width: 0; + border-right-color: transparent; + opacity: 0; + pointer-events: none; } -.project-sidebar__header { - border-bottom: 1px solid var(--color-border-subtle); - background: linear-gradient( - 180deg, - color-mix(in srgb, var(--color-accent) 4%, transparent) 0%, - transparent 100% - ); -} - -.project-sidebar__eyebrow { - font-size: 10px; - letter-spacing: 0.16em; - text-transform: uppercase; - color: var(--color-accent); -} - -.project-sidebar__title-row { +.project-sidebar__compact-hdr { display: flex; - align-items: flex-start; + align-items: center; justify-content: space-between; - gap: 12px; - margin-top: 6px; + padding: 10px 12px 8px; + border-bottom: 1px solid var(--sidebar-border); + flex-shrink: 0; } -.project-sidebar__title { - font-size: 13px; - line-height: 1.1; - letter-spacing: -0.02em; - font-weight: 600; - color: var(--color-text-primary); -} - -.project-sidebar__subtitle { - margin-top: 3px; - max-width: 18ch; - font-size: 10px; - line-height: 1.4; +.project-sidebar__sect-label { + font-size: 11px; + font-weight: 500; + letter-spacing: 0.06em; + text-transform: uppercase; color: var(--color-text-muted); } -.project-sidebar__badge { - display: inline-flex; - min-width: 22px; - height: 22px; +.project-sidebar__add-btn { + display: flex; align-items: center; justify-content: center; - border: 1px solid var(--color-border-default); - background: color-mix(in srgb, var(--color-bg-base) 42%, transparent); - font-size: 10px; - font-weight: 600; - color: var(--color-text-primary); - font-variant-numeric: tabular-nums; + width: 20px; + height: 20px; + border-radius: 4px; + color: var(--color-text-muted); + background: transparent; + border: none; + cursor: pointer; + transition: background 100ms, color 100ms; + flex-shrink: 0; } -.project-sidebar__summary { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); +.project-sidebar__add-btn svg { + width: 12px; + height: 12px; +} + +.project-sidebar__add-btn:hover { + background: var(--sidebar-row-hover); + color: var(--color-text-secondary); +} + +/* ── Project tree ──────────────────────────────────────────────────── */ + +.project-sidebar__tree::-webkit-scrollbar { + width: 3px; +} +.project-sidebar__tree::-webkit-scrollbar-track { + background: transparent; +} +.project-sidebar__tree::-webkit-scrollbar-thumb { + background: var(--color-border-default, var(--color-border-subtle)); + border-radius: 2px; +} + +/* Project accordion item */ +.project-sidebar__project { + border-bottom: 1px solid var(--sidebar-border); +} +.project-sidebar__project:last-child { + border-bottom: none; +} + +/* Project toggle row */ +.project-sidebar__proj-toggle { + width: 100%; + display: flex; + align-items: center; gap: 6px; - margin-top: 10px; + padding: 6px 12px; + background: none; + border: none; + cursor: pointer; + color: var(--color-text-primary); + font-family: var(--font-sans); + font-size: 12px; + font-weight: 500; + text-align: left; + transition: background 80ms; } -.project-sidebar__metric { +.project-sidebar__proj-toggle:hover { + background: var(--sidebar-row-hover); +} + +.project-sidebar__proj-toggle--active { + color: var(--color-accent-amber); +} + +/* Project chevron: rotates 90deg when open */ +.project-sidebar__proj-chevron { + flex-shrink: 0; + color: var(--color-text-muted); + transition: transform 180ms cubic-bezier(0.23, 1, 0.32, 1); +} + +.project-sidebar__proj-chevron--open { + transform: rotate(90deg); +} + +/* Project name */ +.project-sidebar__proj-name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Project badge (session count) */ +.project-sidebar__proj-badge { + flex-shrink: 0; + font-size: 10px; + font-family: var(--font-mono); + font-weight: 500; + padding: 1px 5px; + border-radius: 3px; + background: var(--color-bg-subtle); + border: 1px solid var(--color-border-subtle); + color: var(--color-text-muted); + line-height: 1.4; +} + +.project-sidebar__proj-badge--active { + background: var(--color-accent-amber-dim); + border-color: var(--color-accent-amber-border); + color: var(--color-accent-amber); +} + +/* Session list */ +.project-sidebar__sessions { display: flex; flex-direction: column; - gap: 2px; - padding: 6px 6px 5px; - border: 1px solid var(--color-border-subtle); - background: color-mix(in srgb, var(--color-bg-base) 40%, transparent); + gap: 1px; + padding: 2px 0 4px; } -.project-sidebar__metric-value { - font-size: 13px; - line-height: 1; - letter-spacing: -0.03em; - font-weight: 600; - color: var(--color-text-primary); - font-variant-numeric: tabular-nums; -} - -.project-sidebar__metric-label { - font-size: 9px; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--color-text-muted); -} - -.project-sidebar__divider { - border-top: 1px solid var(--color-border-subtle); -} - -.project-sidebar__item { - position: relative; - border: 1px solid transparent; - background: transparent; -} - -.project-sidebar__item:hover { - background: var(--color-hover-overlay); - border-color: var(--color-border-subtle); -} - -.project-sidebar__item--active { - border-color: color-mix(in srgb, var(--color-accent) 24%, var(--color-border-default)); - background: linear-gradient( - 90deg, - color-mix(in srgb, var(--color-accent) 16%, transparent), - transparent 90% - ); -} - -.project-sidebar__count { - border: 1px solid var(--color-border-subtle); - background: color-mix(in srgb, var(--color-bg-base) 40%, transparent); -} - -.project-sidebar__children { - position: relative; - margin-top: 4px; - border-left: 1px solid color-mix(in srgb, var(--color-border-subtle) 82%, transparent); -} - -.project-sidebar__session { - position: relative; - margin: 2px 0 2px 8px; - border: 1px solid transparent; - background: transparent; -} - -.project-sidebar__session:hover { - border-color: var(--color-border-subtle); - background: linear-gradient( - 90deg, - color-mix(in srgb, var(--color-hover-overlay) 90%, transparent), - transparent 100% - ); -} - -.project-sidebar__session--active { - border-color: color-mix(in srgb, var(--color-accent) 18%, var(--color-border-subtle)); - background: linear-gradient( - 90deg, - color-mix(in srgb, var(--color-accent) 12%, transparent), - transparent 88% - ); - box-shadow: inset 2px 0 0 var(--color-accent); -} - -.project-sidebar__session::before { - content: ""; - position: absolute; - left: -9px; - top: 50%; - width: 8px; - height: 1px; - background: var(--color-border-subtle); - transform: translateY(-50%); -} - -.project-sidebar__session-tone { - flex-shrink: 0; - padding: 2px 6px; - border: 1px solid var(--color-border-subtle); - background: color-mix(in srgb, var(--color-bg-base) 40%, transparent); - font-size: 9px; - font-weight: 700; - letter-spacing: 0.1em; - text-transform: uppercase; - color: var(--color-text-muted); -} - -.project-sidebar__session-id { - opacity: 0; - color: var(--color-text-tertiary); - transition: - opacity 0.15s ease, - color 0.15s ease; -} - -.project-sidebar__session:hover .project-sidebar__session-id, -.project-sidebar__session--active .project-sidebar__session-id { - opacity: 0.75; -} - -.project-sidebar__session--active .project-sidebar__session-tone { - border-color: color-mix(in srgb, var(--color-accent) 24%, var(--color-border-subtle)); - background: color-mix(in srgb, var(--color-accent) 10%, transparent); - color: var(--color-accent); -} - -.project-sidebar__collapse-btn, -.project-sidebar__collapsed-toggle { - display: inline-flex; +/* Session row: dot + label + status */ +.project-sidebar__sess-row { + display: flex; align-items: center; - justify-content: center; - gap: 8px; + gap: 7px; + padding: 4px 12px 4px 26px; width: 100%; - border: 1px solid var(--color-border-subtle); - background: color-mix(in srgb, var(--color-bg-base) 40%, transparent); + border: 0; + background: transparent; + cursor: pointer; + transition: background 80ms; + text-align: left; +} + +.project-sidebar__sess-row:hover { + background: var(--sidebar-row-hover); +} + +.project-sidebar__sess-row--active { + background: transparent; +} + +/* Session label */ +.project-sidebar__sess-label { + flex: 1; + font-size: 11.5px; color: var(--color-text-secondary); - font-size: 11px; - font-weight: 600; - transition: - border-color 0.12s ease, - background 0.12s ease, - color 0.12s ease; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } -.project-sidebar__collapse-btn { - padding: 9px 10px; -} - -.project-sidebar__collapsed-toggle { - width: 36px; - height: 36px; -} - -.project-sidebar__collapse-btn:hover, -.project-sidebar__collapsed-toggle:hover { - border-color: var(--color-border-default); - background: var(--color-hover-overlay); +.project-sidebar__sess-label--active { color: var(--color-text-primary); + font-weight: 500; } -.project-sidebar__collapsed-project { - position: relative; +/* Session status text */ +.project-sidebar__sess-status { + font-size: 10px; + font-family: var(--font-mono); + color: var(--color-text-muted); + flex-shrink: 0; +} + +/* Empty state inside project */ +.project-sidebar__empty { + padding: 18px 12px; + text-align: center; + color: var(--color-text-muted); + font-size: 11px; +} + +.project-sidebar__footer { + margin-top: auto; + display: flex; + align-items: center; + gap: 8px; + border-top: 1px solid var(--sidebar-border); + padding: 8px 12px; + flex-shrink: 0; +} + +.project-sidebar__theme-toggle { display: inline-flex; - width: 36px; - height: 36px; + height: 27px; + width: 27px; + padding: 0; align-items: center; justify-content: center; - border: 1px solid var(--color-border-subtle); - border-radius: 8px; - background: var(--color-bg-surface); - cursor: pointer; + gap: 0; + border: 1px solid var(--color-border-default); + border-radius: 5px; + background: transparent; + color: var(--color-text-muted); transition: - border-color 0.15s ease, - background 0.15s ease, - box-shadow 0.15s ease; + background 100ms ease-out, + color 100ms ease-out, + border-color 100ms ease-out; } -.project-sidebar__collapsed-project:hover { - border-color: var(--color-border-default); - background: var(--color-bg-elevated); - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); -} - -.project-sidebar__collapsed-project--active { - border-color: color-mix(in srgb, var(--color-accent) 35%, var(--color-border-default)); - background: color-mix(in srgb, var(--color-accent) 8%, var(--color-bg-elevated)); - box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-accent) 12%, transparent); -} - -.project-sidebar__avatar { - font-size: 13px; - font-weight: 600; - line-height: 1; +.project-sidebar__theme-toggle:hover { + background: var(--sidebar-row-hover); color: var(--color-text-secondary); - user-select: none; + border-color: var(--color-border-strong); } -.project-sidebar__collapsed-project--active .project-sidebar__avatar { - color: var(--color-accent); +.project-sidebar__theme-toggle svg { + width: 14px; + height: 14px; + flex-shrink: 0; } -.project-sidebar__health-indicator { - position: absolute; - top: -2px; - right: -2px; - width: 7px; - height: 7px; - border-radius: 50%; - border: 1.5px solid var(--color-bg-surface); - box-sizing: content-box; +.project-sidebar__theme-label { + font-size: 11px; + color: var(--color-text-muted); } /* ── Kanban board ────────────────────────────────────────────────────── */ .kanban-board { - display: flex; + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 8px; - height: calc(100vh - 280px); - height: calc(100dvh - 280px); + height: calc(100vh - 240px); + height: calc(100dvh - 240px); overflow-x: auto; padding-bottom: 8px; } .kanban-board-wrap { position: relative; - margin-top: 12px; + margin-top: 14px; + overflow-x: auto; } .board-section-head { @@ -1862,7 +3425,7 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { .board-section-head__title { font-size: 12px; font-weight: 700; - letter-spacing: 0.16em; + letter-spacing: 0.06em; text-transform: uppercase; color: var(--color-text-secondary); } @@ -1898,95 +3461,75 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { } .kanban-column { - flex: 1 0 260px; - min-width: 260px; - max-width: 420px; display: flex; + min-width: 0; flex-direction: column; - border: 1px solid var(--color-border-default); - border-radius: 0; + border: 1px solid var(--color-border-subtle); + border-radius: 7px; background: var(--color-column-bg); - padding: 4px; - box-shadow: - inset 0 1px 0 color-mix(in srgb, white 2%, transparent), - 0 16px 34px rgba(0, 0, 0, 0.05); + overflow: hidden; } -.kanban-column[data-level="merge"] { - background: linear-gradient( - 180deg, - color-mix(in srgb, var(--color-status-ready) 4%, var(--color-column-bg)) 0%, - var(--color-column-bg) 100% - ); -} - -.dark .kanban-column { - border-color: rgba(170, 195, 230, 0.14); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.02), - 0 18px 42px rgba(0, 0, 0, 0.24); -} - -.dark .kanban-column[data-level="merge"] { - background: linear-gradient( - 180deg, - color-mix(in srgb, var(--color-status-ready) 7%, var(--color-column-bg)) 0%, - var(--color-column-bg) 100% - ); -} .kanban-column__header { - margin-bottom: 14px; - padding: 2px 2px 0; + flex-shrink: 0; + padding: 8px 10px 7px; + border-bottom: 1px solid var(--color-border-subtle); } .kanban-column__title-row { display: flex; align-items: center; - gap: 10px; + gap: 6px; } .kanban-column__dot { - width: 8px; - height: 8px; + width: 6px; + height: 6px; border-radius: 999px; } .kanban-column__title { - font-size: 20px; - font-weight: 600; - letter-spacing: normal; - text-transform: none; - color: var(--color-text-primary); + font-size: 10.5px; + font-weight: 500; + letter-spacing: 0.05em; + text-transform: uppercase; + font-family: var(--font-mono); + color: var(--color-text-muted); } .kanban-column__count { margin-left: auto; - min-width: 28px; - padding: 4px 8px; + padding: 0 4px; border: 1px solid var(--color-border-subtle); - border-radius: 0; - background: color-mix(in srgb, var(--color-bg-base) 72%, transparent); - font-size: 11px; - font-weight: 700; - line-height: 1; - color: var(--color-text-secondary); - font-variant-numeric: tabular-nums; - text-align: center; + border-radius: 3px; + background: var(--color-bg-subtle); + font-family: var(--font-mono); + font-size: 10px; + line-height: 1.6; + color: var(--color-text-muted); } .kanban-column__caption { - display: block; - margin-top: 7px; - font-size: 12px; - line-height: 1.5; - color: var(--color-text-muted); + display: none; } .kanban-column-body { flex: 1; overflow-y: auto; - padding: 2px; + padding: 6px; +} + +.kanban-column-body > div { + display: flex; + flex-direction: column; + gap: 5px; +} + +.kanban-column__stack { + display: flex; + flex-direction: column; + gap: 5px; } .kanban-column-body::-webkit-scrollbar { @@ -1997,7 +3540,7 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { } .kanban-column-body::-webkit-scrollbar-thumb { background: var(--color-scrollbar); - border-radius: 2px; + border-radius: 0; } /* ── Done / Terminated collapsible bar ────────────────────────────────── */ @@ -2010,22 +3553,32 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { padding: 8px 0; background: none; border: none; + border-bottom: 1px solid var(--color-border-subtle); cursor: pointer; - font-size: 12px; - font-weight: 700; - letter-spacing: 0.16em; - text-transform: uppercase; color: var(--color-text-muted); transition: color 0.15s ease; } +.done-bar__label { + font-size: 10.5px; + font-weight: 500; + letter-spacing: 0.05em; + text-transform: uppercase; + font-family: var(--font-mono); + color: var(--color-text-muted); +} + .done-bar__toggle:hover { + color: var(--color-text-primary); +} + +.done-bar__toggle:hover .done-bar__label { color: var(--color-text-secondary); } .done-bar__chevron { - width: 14px; - height: 14px; + width: 12px; + height: 12px; flex-shrink: 0; transition: transform 0.15s ease; } @@ -2035,83 +3588,146 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { } .done-bar__count { + margin-left: auto; flex-shrink: 0; - min-width: 18px; - height: 18px; - display: inline-flex; - align-items: center; - justify-content: center; + font-family: var(--font-mono); font-size: 10px; - font-weight: 700; - border-radius: 999px; - background: var(--color-border-subtle); color: var(--color-text-muted); - padding: 0 5px; -} - -.done-bar__rule { - flex: 1; - height: 1px; - background: var(--color-border-subtle); } .done-bar__cards { display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 8px; - margin-top: 10px; + margin-top: 12px; +} + +/* ── Done card (compact grid card) ──────────────────────────────────── */ + +.done-card { + background: var(--color-bg-card); + border: 1px solid var(--color-border-subtle); + border-radius: 6px; + padding: 8px 10px; + opacity: 0.75; + transition: opacity 0.15s; + display: flex; + flex-direction: column; + gap: 5px; +} + +.done-card:hover { + opacity: 1; +} + +.done-card__title { + font-size: 11.5px; + font-weight: 500; + color: var(--color-text-secondary); + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.done-card__meta { + display: flex; + align-items: center; + gap: 6px; +} + +.done-card__badge { + font-size: 9.5px; + font-weight: 600; + padding: 1px 6px; + border-radius: 4px; + white-space: nowrap; + flex-shrink: 0; +} + +.done-card__badge--merged { + background: var(--color-tint-neutral); + color: var(--color-text-secondary); +} + +.done-card__badge--terminated { + background: var(--color-tint-red); + color: var(--color-accent-red); +} + +.done-card__pr { + font-size: 9.5px; + font-family: var(--font-mono); + color: var(--color-text-muted); + display: flex; + align-items: center; + gap: 3px; + text-decoration: none; + flex-shrink: 0; +} + +.done-card__pr:hover { + color: var(--color-text-secondary); +} + +.done-card__age { + font-size: 9.5px; + color: var(--color-text-muted); + margin-left: auto; + white-space: nowrap; + flex-shrink: 0; +} + +.done-card__restore { + padding: 2px 7px; + font-size: 10px; + font-weight: 600; + border: 1px solid var(--color-border-default); + background: transparent; + color: var(--color-text-muted); + border-radius: 4px; + cursor: pointer; + white-space: nowrap; + transition: border-color 0.15s, color 0.15s; + flex-shrink: 0; +} + +.done-card__restore:hover { + border-color: var(--color-accent); + color: var(--color-accent); } /* ── Session card zone glow (hover) ──────────────────────────────────── */ .kanban-column__empty { - display: flex; - min-height: 132px; - align-items: center; - justify-content: center; - border: 1px dashed color-mix(in srgb, var(--color-border-default) 82%, transparent); - border-radius: 0; - background: linear-gradient( - 180deg, - color-mix(in srgb, var(--color-bg-base) 28%, transparent) 0%, - transparent 100% - ); + display: none; } .kanban-column__empty-label { - font-size: 12px; - letter-spacing: 0.08em; - text-transform: none; - color: var(--color-text-tertiary); + display: none; } /* Card glow effects removed for Linear-clean look */ - @media (max-width: 960px) { .dashboard-hero__content { - padding: 12px 14px; + padding: 16px 28px 10px; } .dashboard-hero__primary { - width: 100%; - flex-direction: column; + width: auto; + flex-direction: row; gap: 10px; } .dashboard-hero__meta { - width: 100%; - align-items: flex-start; - justify-content: space-between; - } - - .dashboard-stat-cards { - width: 100%; + justify-content: flex-end; } .kanban-board { height: auto; - min-height: calc(100vh - 280px); + min-height: calc(100vh - 240px); } .session-card--fixed { @@ -2129,15 +3745,11 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { @media (max-width: 640px) { .dashboard-title { - font-size: 26px; - } - - .dashboard-stat-cards { - gap: 6px; + font-size: 19px; } .kanban-column { - min-width: min(86vw, 320px); + min-width: min(86vw, 280px); } .session-card--fixed { @@ -2228,9 +3840,6 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { /* -- Dashboard main -- */ .dashboard-main { - padding-left: max(12px, env(safe-area-inset-left, 0px)); - padding-right: max(12px, env(safe-area-inset-right, 0px)); - padding-top: 12px; padding-bottom: calc(84px + env(safe-area-inset-bottom, 0px)); } @@ -2238,15 +3847,16 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { .dashboard-hero { margin-bottom: 8px; border: none; + border-bottom: none; background: transparent; box-shadow: none; } .dashboard-hero__content { display: grid; - grid-template-columns: 36px minmax(0, 1fr) 36px; + grid-template-columns: 44px minmax(0, 1fr) 44px; align-items: center; - padding: 0; + padding: 0 12px; gap: 8px; min-height: 0; } @@ -2257,10 +3867,12 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { } .dashboard-hero__heading { + display: block; min-width: 0; } .dashboard-hero__copy { + display: block; min-width: 0; } @@ -2276,47 +3888,36 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { display: none; } - /* Hide stat cards on mobile — replaced by the action strip */ - .dashboard-stat-cards { - display: none; - } - - /* Keep stat cards visible on pages without an action-strip replacement */ - .dashboard-stat-cards--persist-mobile { - display: flex; - } - .dashboard-hero__meta { display: flex; align-items: center; justify-content: flex-end; } - .dashboard-hero__meta > div { + .dashboard-topbar-meta { gap: 0; } - .dashboard-hero__meta button { - width: 36px; - height: 36px; - border: none; - background: none; - color: var(--color-text-secondary); - box-shadow: none; - padding: 0; + .dashboard-topbar-summary, + .dashboard-topbar-link { + display: none; } .mobile-menu-toggle { - width: 36px; - height: 36px; - border: none; - background: none; + width: 44px; + min-width: 44px; + height: 44px; + min-height: 44px; + padding: 0; + border: 1px solid var(--color-border-default); + background: color-mix(in srgb, var(--color-bg-surface) 92%, transparent); color: var(--color-text-secondary); + border-radius: 10px; } .mobile-menu-toggle:hover { - border: none; - background: none; + border-color: var(--color-border-strong); + background: var(--color-hover-overlay); color: var(--color-text-primary); } @@ -2335,8 +3936,7 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { 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 { @@ -2405,13 +4005,13 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { .mobile-filter-row { display: flex; - align-items: center; gap: 8px; - overflow-x: auto; - -webkit-overflow-scrolling: touch; - scrollbar-width: none; margin-bottom: 16px; - padding-bottom: 2px; + padding: 0 0 2px; + overflow-x: auto; + scrollbar-width: none; + -webkit-overflow-scrolling: touch; + min-width: 0; } .mobile-filter-row::-webkit-scrollbar { @@ -2423,12 +4023,14 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { align-items: center; justify-content: center; min-height: 32px; - padding: 0 12px; - border: 1px solid var(--color-border-default); - background: color-mix(in srgb, var(--color-bg-surface) 88%, transparent); + min-width: 0; + padding: 6px 14px; + border: 1px solid var(--color-border-subtle); + border-radius: 16px; + background: var(--color-bg-surface); color: var(--color-text-secondary); font-size: 11px; - font-weight: 600; + font-weight: 500; white-space: nowrap; cursor: pointer; transition: @@ -2438,8 +4040,8 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { } .mobile-filter-chip[data-active="true"] { - border-color: color-mix(in srgb, var(--color-accent) 28%, var(--color-border-default)); - background: color-mix(in srgb, var(--color-accent) 10%, transparent); + border-color: color-mix(in srgb, var(--color-accent) 30%, transparent); + background: color-mix(in srgb, var(--color-accent) 12%, transparent); color: var(--color-accent); } @@ -2503,6 +4105,28 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { color: var(--color-text-secondary); } + /* ── Mobile action pill tone colors ──────────────────────────────────── */ + + .mobile-action-pill__dot[data-level="respond"] { + background: var(--color-status-respond); + } + .mobile-action-pill__dot[data-level="merge"] { + background: var(--color-status-ready); + } + .mobile-action-pill__dot[data-level="review"] { + background: var(--color-accent-orange); + } + + .mobile-action-pill__count[data-level="respond"] { + color: var(--color-status-respond); + } + .mobile-action-pill__count[data-level="merge"] { + color: var(--color-status-ready); + } + .mobile-action-pill__count[data-level="review"] { + color: var(--color-accent-orange); + } + /* -- Kanban board: stack columns vertically -- */ .kanban-board { flex-direction: column; @@ -2708,7 +4332,9 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { } /* -- iOS auto-zoom prevention on input focus -- */ - input, textarea, select { + input, + textarea, + select { font-size: 16px; } @@ -2946,6 +4572,38 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { flex-shrink: 0; } + .accordion-header__dot[data-level="working"] { + background: var(--color-status-working); + } + .accordion-header__dot[data-level="pending"] { + background: var(--color-status-attention); + } + .accordion-header__dot[data-level="review"] { + background: var(--color-accent-orange); + } + .accordion-header__dot[data-level="respond"] { + background: var(--color-status-respond); + } + .accordion-header__dot[data-level="merge"] { + background: var(--color-status-ready); + } + + .mobile-session-row__dot[data-level="working"] { + background: var(--color-status-working); + } + .mobile-session-row__dot[data-level="pending"] { + background: var(--color-status-attention); + } + .mobile-session-row__dot[data-level="review"] { + background: var(--color-accent-orange); + } + .mobile-session-row__dot[data-level="respond"] { + background: var(--color-status-respond); + } + .mobile-session-row__dot[data-level="merge"] { + background: var(--color-status-ready); + } + .accordion-header__label { font-size: 16px; font-weight: 600; @@ -3007,22 +4665,23 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { .mobile-pr-list { display: flex; flex-direction: column; - border: 1px solid color-mix(in srgb, var(--color-border-default) 82%, transparent); - background: var(--color-bg-surface); + gap: 0; } .mobile-pr-card { - display: flex; - flex-direction: column; - gap: 5px; - padding: 11px 12px; - border-top: 1px solid var(--color-border-subtle); + display: block; + padding: 10px 0; + border-bottom: 1px solid color-mix(in srgb, var(--color-border-subtle) 92%, transparent); + background: transparent; color: var(--color-text-primary); text-decoration: none; + transition: + opacity 0.14s ease, + background 0.14s ease; } - .mobile-pr-card:first-child { - border-top: none; + .mobile-pr-card:active { + background: color-mix(in srgb, var(--color-bg-elevated) 42%, transparent); } .mobile-pr-card__line { @@ -3030,42 +4689,61 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { align-items: center; gap: 8px; min-width: 0; + margin-bottom: 4px; } .mobile-pr-card__number { flex-shrink: 0; font-family: var(--font-mono); - font-size: 11px; - font-weight: 700; + font-size: 12px; + font-weight: 500; color: var(--color-accent); } .mobile-pr-card__title { flex: 1; min-width: 0; + font-size: 13px; + font-weight: 500; + line-height: 1.35; + white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - white-space: nowrap; - font-size: 12px; - font-weight: 600; - } - - .mobile-pr-card__size { - flex-shrink: 0; - padding: 1px 6px; - border: 1px solid var(--color-border-default); - background: var(--color-bg-subtle); - font-size: 10px; - font-weight: 700; - color: var(--color-text-secondary); } .mobile-pr-card__meta { display: flex; - flex-wrap: wrap; - gap: 8px; + align-items: center; + gap: 10px; + min-width: 0; + font-size: 11px; + color: var(--color-text-secondary); + } + + .mobile-pr-card__metric-value { + display: inline-flex; + align-items: center; + gap: 4px; + flex-shrink: 0; + font-size: 11px; + font-weight: 500; + color: var(--color-text-secondary); + } + + .mobile-pr-card__review { + min-width: 0; + font-size: 11px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .mobile-pr-card__diff { + font-family: var(--font-mono); font-size: 10px; - color: var(--color-text-muted); + margin-left: auto; + color: var(--color-text-secondary); + flex-shrink: 0; } .mx-auto.max-w-\[900px\] > h2 { @@ -3075,6 +4753,483 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { color: var(--color-text-secondary); letter-spacing: 0.12em; } + + /* ── Mobile Priority Feed (Dashboard) ───────────────────────────────── */ + + .mobile-feed { + display: flex; + flex-direction: column; + gap: 8px; + padding-bottom: 8px; + } + + .mobile-feed-card { + display: flex; + gap: 12px; + background: var(--card-bg); + border: 1px solid var(--card-border); + border-radius: 6px; + padding: 12px; + text-decoration: none; + color: var(--color-text-primary); + box-shadow: var(--card-shadow); + transition: + background 0.12s ease, + border-color 0.12s ease, + box-shadow 0.12s ease; + } + + .mobile-feed-card:active { + background: var(--card-expanded-bg); + border-color: var(--color-border-strong); + box-shadow: var(--card-shadow-hover); + } + + .mobile-feed-card__strip { + width: 3px; + border-radius: 2px; + flex-shrink: 0; + align-self: stretch; + } + + .mobile-feed-card__strip[data-level="respond"] { + background: var(--color-status-respond); + } + .mobile-feed-card__strip[data-level="merge"] { + background: var(--color-status-ready); + } + .mobile-feed-card__strip[data-level="review"] { + background: var(--color-accent-orange); + } + .mobile-feed-card__strip[data-level="pending"] { + background: var(--color-status-attention); + } + .mobile-feed-card__strip[data-level="working"] { + background: var(--color-status-working); + } + + .mobile-feed-card__content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 6px; + } + + .mobile-feed-card__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + } + + .mobile-feed-card__id { + display: flex; + align-items: center; + gap: 6px; + font-family: var(--font-mono); + font-size: 11px; + color: var(--color-text-muted); + } + + .mobile-feed-card__time { + font-family: var(--font-mono); + font-size: 10px; + color: var(--color-text-tertiary); + flex-shrink: 0; + } + + .mobile-feed-card__title { + font-size: 13px; + font-weight: 500; + line-height: 1.4; + color: var(--color-text-primary); + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-line-clamp: 2; + overflow: hidden; + } + + .mobile-feed-card__meta { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + } + + .mobile-feed-card__branch { + font-family: var(--font-mono); + font-size: 10px; + color: var(--color-text-secondary); + background: color-mix(in srgb, var(--color-bg-subtle) 88%, var(--color-bg-surface)); + border: 1px solid color-mix(in srgb, var(--color-border-subtle) 88%, transparent); + padding: 2px 6px; + border-radius: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 160px; + } + + .mobile-feed-card__pr { + font-family: var(--font-mono); + font-size: 11px; + color: var(--color-accent); + text-decoration: none; + } + + .mobile-feed-card__diff { + font-family: var(--font-mono); + font-size: 10px; + } + + .mobile-feed-card__pills { + display: flex; + gap: 6px; + flex-wrap: wrap; + } + + .mobile-feed-card__pill { + display: inline-block; + font-size: 10px; + font-family: var(--font-mono); + padding: 2px 6px; + border: 1px solid transparent; + border-radius: 4px; + font-weight: 500; + } + + .mobile-feed-card__pill--green { + background: color-mix(in srgb, var(--color-accent-green) 12%, transparent); + border-color: color-mix(in srgb, var(--color-accent-green) 22%, transparent); + color: var(--color-accent-green); + } + .mobile-feed-card__pill--amber { + background: color-mix(in srgb, var(--color-status-attention) 12%, transparent); + border-color: color-mix(in srgb, var(--color-status-attention) 22%, transparent); + color: var(--color-status-attention); + } + .mobile-feed-card__pill--red { + background: color-mix(in srgb, var(--color-accent-red) 12%, transparent); + border-color: color-mix(in srgb, var(--color-accent-red) 22%, transparent); + color: var(--color-accent-red); + } + .mobile-feed-card__pill--accent { + background: color-mix(in srgb, var(--color-accent) 12%, transparent); + border-color: color-mix(in srgb, var(--color-accent) 22%, transparent); + color: var(--color-accent); + } + + .mobile-feed-empty { + text-align: center; + padding: 32px 16px; + color: var(--color-text-muted); + font-size: 13px; + } + + /* ── Mobile Session Detail: Terminal First ──────────────────────────── */ + + .session-detail--terminal-first { + position: relative; + height: 100vh; + height: 100dvh; + overflow: hidden; + background: var(--color-bg-base); + } + + .session-detail__floating-header { + position: absolute; + top: 0; + left: 0; + right: 0; + display: flex; + align-items: center; + gap: 10px; + padding: max(env(safe-area-inset-top, 8px), 8px) 16px 8px; + background: color-mix(in srgb, var(--color-bg-base) 84%, transparent); + border-bottom: 1px solid color-mix(in srgb, var(--color-border-subtle) 88%, transparent); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + z-index: 10; + } + + .session-detail__back { + color: var(--color-accent); + font-size: 18px; + text-decoration: none; + width: 32px; + height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + + .session-detail__status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + } + + .session-detail__session-id { + font-family: var(--font-mono); + font-size: 13px; + color: var(--color-text-primary); + } + + .session-detail__status-pill { + font-size: 10px; + font-family: var(--font-mono); + padding: 2px 8px; + border: 1px solid color-mix(in srgb, currentColor 20%, transparent); + border-radius: 10px; + } + + .session-detail__time { + margin-left: auto; + font-family: var(--font-mono); + font-size: 11px; + color: var(--color-text-tertiary); + flex-shrink: 0; + } + + .session-detail__terminal-full { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + padding-top: calc(max(env(safe-area-inset-top, 8px), 8px) + 40px); + padding-bottom: calc(80px + env(safe-area-inset-bottom, 0px)); + } + + .session-detail__terminal-full--with-sheet { + padding-bottom: calc(120px + env(safe-area-inset-bottom, 0px)); + } + + .session-detail__terminal-full .direct-terminal, + .session-detail__terminal-full .direct-terminal__container, + .session-detail__terminal-full .terminal-exited-placeholder, + .session-detail__terminal-full .session-detail-terminal-placeholder { + height: 100% !important; + min-height: 0; + border-radius: 0; + border: none; + } + + .session-detail__bottom-sheet { + position: absolute; + bottom: calc(60px + env(safe-area-inset-bottom, 0px)); + left: 0; + right: 0; + background: color-mix(in srgb, var(--color-bg-surface) 96%, var(--color-bg-base)); + border-top: 1px solid color-mix(in srgb, var(--color-border-default) 88%, transparent); + border-radius: 12px 12px 0 0; + padding: 8px 16px 12px; + box-shadow: 0 -10px 30px rgba(0, 0, 0, 0.22); + z-index: 10; + } + + .session-detail__sheet-handle { + width: 36px; + height: 4px; + background: color-mix(in srgb, var(--color-text-tertiary) 72%, transparent); + border-radius: 2px; + margin: 0 auto 10px; + } + + .session-detail__sheet-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + } + + .session-detail__sheet-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--color-text-secondary); + } + + .session-detail__sheet-pr { + font-family: var(--font-mono); + font-size: 12px; + color: var(--color-accent); + text-decoration: none; + } + + .session-detail__sheet-ci-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; + } + + /* ── Mobile PR Dense Rows enhancements ─────────────────────────────── */ + + .mobile-pr-mobile-layout { + display: flex; + flex-direction: column; + gap: 10px; + padding-bottom: 8px; + } + + .mobile-pr-group { + display: flex; + flex-direction: column; + gap: 0; + } + + .mobile-pr-section-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + font-family: var(--font-sans); + font-size: 10px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--color-text-tertiary); + padding: 12px 0 6px; + } + + .mobile-pr-card--muted { + opacity: 0.72; + } + + .mobile-pr-card__ci-dot { + display: inline-block; + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; + vertical-align: middle; + margin-right: 4px; + } + + .mobile-pr-filter-tabs { + display: flex; + gap: 0; + overflow-x: auto; + padding: 0; + margin-bottom: 0; + scrollbar-width: none; + border-bottom: 1px solid color-mix(in srgb, var(--color-border-subtle) 92%, transparent); + } + + .mobile-pr-filter-tabs::-webkit-scrollbar { + display: none; + } + + .mobile-pr-filter-tab { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 6px; + flex-shrink: 0; + min-height: 38px; + min-width: 0; + padding: 10px 12px; + font-size: 12px; + font-weight: 500; + color: var(--color-text-tertiary); + background: transparent; + border: none; + cursor: pointer; + white-space: nowrap; + transition: color 0.12s ease; + position: relative; + } + + .mobile-pr-filter-tab[data-active="true"] { + color: var(--color-accent); + } + + .mobile-pr-filter-tab[data-active="true"]::after { + content: ""; + position: absolute; + bottom: 0; + left: 8px; + right: 8px; + height: 2px; + background: var(--color-accent); + border-radius: 1px; + } + + .mobile-pr-filter-tab__count { + font-family: var(--font-mono); + font-size: 10px; + opacity: 0.7; + } + + @media (max-width: 360px) { + .mobile-pr-filter-tabs { + gap: 6px; + padding-right: 10px; + scroll-padding-right: 10px; + } + + .mobile-pr-filter-tab { + padding: 6px 12px; + } + } + + .mobile-pr-empty { + padding: 22px 0; + color: var(--color-text-muted); + font-size: 12px; + text-align: center; + } + + .mobile-pr-page-header { + padding: 8px 0 0; + margin-bottom: 2px; + } + + .mobile-pr-page-header__top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 4px; + } + + .mobile-pr-page-header__title-row { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + } + + .mobile-pr-page-header__title { + font-family: var(--font-mono); + font-size: 15px; + font-weight: 500; + letter-spacing: -0.02em; + color: var(--color-text-primary); + } + + .mobile-pr-page-header__meta { + display: flex; + align-items: center; + gap: 10px; + flex-shrink: 0; + } + + .mobile-pr-page-header__count { + font-family: var(--font-mono); + font-size: 12px; + color: var(--color-text-secondary); + } + + .mobile-pr-page-header__subtitle { + font-size: 11px; + color: var(--color-text-tertiary); + } } /* ── Phone breakpoint — 480px ────────────────────────────────────────── */ @@ -3097,7 +5252,7 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { } .kanban-column__title { - font-size: 16px; + font-size: 10px; } .session-page-header h1 { @@ -3105,7 +5260,7 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { } /* Session detail terminal full-width */ - .session-detail-page .mx-auto { + .session-detail-layout { padding-left: 8px; padding-right: 8px; } @@ -3171,7 +5326,7 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { color: var(--color-text-primary); padding: 10px 16px; max-width: 340px; - border-radius: 6px; + border-radius: 0; font-size: 13px; line-height: 1.4; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); @@ -3216,7 +5371,7 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { .bottom-sheet-backdrop { position: fixed; inset: 0; - background: rgba(0, 0, 0, 0.5); + background: rgba(0, 0, 0, 0.42); z-index: calc(var(--z-overlay) + 20); } @@ -3226,51 +5381,130 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { left: 0; right: 0; background: var(--color-bg-surface); - border-top: 1px solid var(--color-border-default); + border: 1px solid var(--color-border-subtle); border-bottom: none; - padding: 16px 20px calc(12px + env(safe-area-inset-bottom, 0px)); + border-radius: 12px 12px 0 0; + box-shadow: 0 -14px 36px rgba(0, 0, 0, 0.28); + padding: 10px 16px calc(18px + env(safe-area-inset-bottom, 0px)); z-index: calc(var(--z-overlay) + 21); transform: translateY(0); animation: sheet-in 0.25s ease forwards; } .bottom-sheet__handle { - width: 36px; + width: 42px; height: 4px; - background: var(--color-border-default); - border-radius: 2px; - margin: 0 auto 16px; + background: color-mix(in srgb, var(--color-text-tertiary) 55%, transparent); + border-radius: 999px; + margin: 0 auto 12px; } .bottom-sheet__header { - margin-bottom: 16px; + margin-bottom: 14px; +} + +.bottom-sheet__preview-card { + display: flex; + gap: 12px; + background: var(--card-bg); + border: 1px solid var(--card-border); + border-radius: 6px; + padding: 12px; + box-shadow: var(--card-shadow); + margin-bottom: 12px; +} + +.bottom-sheet__preview-strip { + width: 3px; + border-radius: 2px; + flex-shrink: 0; + align-self: stretch; + background: var(--color-status-attention); +} + +.bottom-sheet__preview-strip[data-level="respond"] { + background: var(--color-status-respond); +} + +.bottom-sheet__preview-strip[data-level="merge"] { + background: var(--color-status-ready); +} + +.bottom-sheet__preview-strip[data-level="review"] { + background: var(--color-accent-orange); +} + +.bottom-sheet__preview-strip[data-level="pending"] { + background: var(--color-status-attention); +} + +.bottom-sheet__preview-strip[data-level="working"] { + background: var(--color-status-working); +} + +.bottom-sheet__preview-strip[data-level="done"] { + background: var(--color-text-tertiary); +} + +.bottom-sheet__preview-content { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 6px; +} + +.bottom-sheet__preview-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.bottom-sheet__preview-id { + font-family: var(--font-mono); + font-size: 11px; + color: var(--color-text-muted); +} + +.bottom-sheet__preview-time { + font-family: var(--font-mono); + font-size: 10px; + color: var(--color-text-tertiary); + flex-shrink: 0; } .bottom-sheet__title { - font-size: 15px; - font-weight: 600; + font-family: var(--font-sans); + font-size: 17px; + line-height: 1.3; + font-weight: 500; + letter-spacing: -0.015em; color: var(--color-text-primary); - margin: 0 0 4px; + margin: 0; } .bottom-sheet__subtitle { font-size: 12px; + line-height: 1.45; color: var(--color-text-secondary); margin: 0; } .bottom-sheet__session-info { - background: var(--color-bg-subtle); - border: 1px solid var(--color-border-subtle); - padding: 10px 12px; - margin-bottom: 20px; + background: color-mix(in srgb, var(--color-bg-subtle) 92%, var(--color-bg-surface)); + border: 1px solid color-mix(in srgb, var(--color-border-subtle) 90%, transparent); + border-radius: 0; + padding: 14px; + margin-bottom: 12px; + box-shadow: none; } .bottom-sheet__session-name { - font-size: 13px; + font-size: 15px; font-weight: 500; color: var(--color-text-primary); - margin-bottom: 4px; + margin-bottom: 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -3282,39 +5516,94 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { flex-wrap: wrap; } +.bottom-sheet__preview-meta { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; +} + +.bottom-sheet__preview-branch { + font-family: var(--font-mono); + font-size: 10px; + color: var(--color-text-secondary); + background: color-mix(in srgb, var(--color-bg-subtle) 88%, var(--color-bg-surface)); + border: 1px solid color-mix(in srgb, var(--color-border-subtle) 88%, transparent); + padding: 2px 6px; + border-radius: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 150px; +} + +.bottom-sheet__preview-pr { + font-family: var(--font-mono); + font-size: 11px; + color: var(--color-accent); +} + +.bottom-sheet__preview-diff { + font-family: var(--font-mono); + font-size: 10px; +} + +.bottom-sheet__preview-diff-add { + color: var(--color-accent-green); +} + +.bottom-sheet__preview-diff-del { + color: var(--color-accent-red); +} + +.bottom-sheet__preview-pills { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + .bottom-sheet__tag { display: inline-flex; align-items: center; - min-height: 24px; - padding: 0 8px; - border: 1px solid var(--color-border-default); + min-height: 22px; + padding: 2px 6px; + border: 1px solid transparent; + border-radius: 4px; background: var(--color-bg-surface); font-size: 10px; - font-weight: 600; - letter-spacing: 0.04em; - text-transform: uppercase; + font-family: var(--font-mono); + font-weight: 500; + letter-spacing: 0; + text-transform: none; color: var(--color-text-secondary); } .bottom-sheet__tag--accent { - color: var(--color-accent); - border-color: color-mix(in srgb, var(--color-accent) 24%, var(--color-border-default)); + background: color-mix(in srgb, var(--color-status-attention) 12%, transparent); + border-color: color-mix(in srgb, var(--color-status-attention) 22%, transparent); + color: var(--color-status-attention); +} + +.bottom-sheet__tag--neutral { + background: color-mix(in srgb, var(--color-accent-green) 12%, transparent); + border-color: color-mix(in srgb, var(--color-accent-green) 22%, transparent); + color: var(--color-accent-green); } .bottom-sheet__tag--mono { font-family: var(--font-mono); text-transform: none; - letter-spacing: 0; + letter-spacing: 0.01em; } .bottom-sheet__summary { - margin: 10px 0 0; + margin: 0 0 12px; font-size: 12px; line-height: 1.5; color: var(--color-text-secondary); display: -webkit-box; -webkit-box-orient: vertical; - -webkit-line-clamp: 1; + -webkit-line-clamp: 2; overflow: hidden; } @@ -3330,11 +5619,18 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { align-items: center; justify-content: center; gap: 8px; - font-size: 14px; + padding: 0 14px; + border-radius: 6px; + font-size: 13px; font-weight: 500; cursor: pointer; border: 1px solid var(--color-border-default); - transition: opacity 0.15s ease; + transition: + background 120ms ease, + border-color 120ms ease, + color 120ms ease, + transform 120ms cubic-bezier(0.23, 1, 0.32, 1), + opacity 120ms ease; } .bottom-sheet__btn-icon { @@ -3344,44 +5640,47 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { } .bottom-sheet__btn:active { - opacity: 0.75; + transform: scale(0.98); } .bottom-sheet__btn--cancel { - background: transparent; + background: var(--color-bg-elevated); color: var(--color-text-secondary); } .bottom-sheet__btn--cancel:hover { background: var(--color-bg-subtle); + color: var(--color-text-primary); } .bottom-sheet__btn--danger { - background: var(--color-status-error); - border-color: var(--color-status-error); + background: var(--color-accent-red, var(--color-status-error)); + border-color: var(--color-accent-red, var(--color-status-error)); color: #ffffff; } .bottom-sheet__btn--danger:hover { - opacity: 0.9; + background: color-mix(in srgb, var(--color-accent-red, var(--color-status-error)) 92%, black 8%); + border-color: color-mix(in srgb, var(--color-accent-red, var(--color-status-error)) 92%, black 8%); } .bottom-sheet__btn--primary { display: inline-flex; align-items: center; justify-content: center; - background: var(--color-accent); - border-color: var(--color-accent); + background: var(--color-accent-blue, var(--color-accent)); + border-color: var(--color-accent-blue, var(--color-accent)); color: #ffffff; text-decoration: none; } .bottom-sheet__btn--primary:hover { - opacity: 0.92; + background: var(--color-accent-hover); + border-color: var(--color-accent-hover); } .bottom-sheet__btn--secondary { - background: transparent; + background: var(--color-bg-elevated); color: var(--color-text-primary); } @@ -3399,7 +5698,9 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { 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; @@ -3413,7 +5714,7 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { .connection-bar--reconnecting { height: 4px; - background: #d4a017; /* amber — no token for this in the design system */ + background: var(--color-status-attention); animation: connection-pulse 1.5s ease-in-out infinite; } @@ -3425,6 +5726,23 @@ html.light .xterm .xterm-viewport:hover::-webkit-scrollbar-thumb:active { } @keyframes connection-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.4; + } +} + +/* ── Reduced motion ─────────────────────────────────────────────────── */ + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } } diff --git a/packages/web/src/app/layout.test.ts b/packages/web/src/app/layout.test.ts new file mode 100644 index 000000000..5c904aa36 --- /dev/null +++ b/packages/web/src/app/layout.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("@/lib/project-name", () => ({ + getProjectName: () => "Agent Orchestrator", +})); + +vi.mock("next/font/google", () => ({ + Geist: () => ({ variable: "--font-geist-sans" }), + JetBrains_Mono: () => ({ variable: "--font-jetbrains-mono" }), +})); + +describe("app layout metadata", () => { + it("exports the themed mobile viewport colors", async () => { + const { viewport } = await import("./layout"); + + expect(viewport.themeColor).toEqual([ + { media: "(prefers-color-scheme: light)", color: "#f5f3f0" }, + { media: "(prefers-color-scheme: dark)", color: "#121110" }, + ]); + }); + + it("builds metadata with the project-aware title and apple web app settings", async () => { + const { generateMetadata } = await import("./layout"); + + await expect(generateMetadata()).resolves.toMatchObject({ + title: { + template: "%s | Agent Orchestrator", + default: "ao | Agent Orchestrator", + }, + appleWebApp: { + capable: true, + statusBarStyle: "black-translucent", + title: "ao | Agent Orchestrator", + }, + }); + }); +}); diff --git a/packages/web/src/app/layout.tsx b/packages/web/src/app/layout.tsx index 0c12d5c1a..144110739 100644 --- a/packages/web/src/app/layout.tsx +++ b/packages/web/src/app/layout.tsx @@ -23,8 +23,8 @@ export const viewport: Viewport = { initialScale: 1, viewportFit: "cover", themeColor: [ - { media: "(prefers-color-scheme: light)", color: "#ffffff" }, - { media: "(prefers-color-scheme: dark)", color: "#0a0d12" }, + { media: "(prefers-color-scheme: light)", color: "#f5f3f0" }, + { media: "(prefers-color-scheme: dark)", color: "#121110" }, ], }; diff --git a/packages/web/src/app/manifest.test.ts b/packages/web/src/app/manifest.test.ts new file mode 100644 index 000000000..bd6e7e62f --- /dev/null +++ b/packages/web/src/app/manifest.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("@/lib/project-name", () => ({ + getProjectName: () => "Agent Orchestrator", +})); + +describe("app manifest", () => { + it("builds the PWA manifest with project-aware naming and icons", async () => { + const { default: manifest } = await import("./manifest"); + + expect(manifest()).toMatchObject({ + name: "ao | Agent Orchestrator", + short_name: "ao", + start_url: "/", + display: "standalone", + background_color: "#121110", + theme_color: "#121110", + icons: [ + { src: "/apple-icon", sizes: "180x180", type: "image/png" }, + { src: "/icon-192", sizes: "192x192", type: "image/png", purpose: "any" }, + { src: "/icon-512", sizes: "512x512", type: "image/png", purpose: "any" }, + ], + }); + }); +}); diff --git a/packages/web/src/app/manifest.ts b/packages/web/src/app/manifest.ts index c5a359a3e..6977692da 100644 --- a/packages/web/src/app/manifest.ts +++ b/packages/web/src/app/manifest.ts @@ -9,8 +9,8 @@ export default function manifest(): MetadataRoute.Manifest { description: "Dashboard for managing parallel AI coding agents", start_url: "/", display: "standalone", - background_color: "#0a0d12", - theme_color: "#0a0d12", + background_color: "#121110", + theme_color: "#121110", icons: [ { src: "/apple-icon", sizes: "180x180", type: "image/png" }, { src: "/icon-192", sizes: "192x192", type: "image/png", purpose: "any" }, diff --git a/packages/web/src/app/sessions/[id]/page.tsx b/packages/web/src/app/sessions/[id]/page.tsx index aceb30352..6a5faa35e 100644 --- a/packages/web/src/app/sessions/[id]/page.tsx +++ b/packages/web/src/app/sessions/[id]/page.tsx @@ -59,6 +59,8 @@ export default function SessionPage() { const [session, setSession] = useState(null); const [zoneCounts, setZoneCounts] = useState(null); const [projectOrchestratorId, setProjectOrchestratorId] = useState(undefined); + const [projects, setProjects] = useState([]); + const [sidebarSessions, setSidebarSessions] = useState([]); const [loading, setLoading] = useState(true); const [routeError, setRouteError] = useState(null); const [sessionMissing, setSessionMissing] = useState(false); @@ -85,6 +87,7 @@ export default function SessionPage() { .then((res) => res.ok ? res.json() : null) .then((data: { projects?: ProjectInfo[] } | null) => { if (data?.projects) { + setProjects(data.projects); setPrefixByProject( new Map(data.projects.map((p) => [p.id, p.sessionPrefix ?? p.id])), ); @@ -185,6 +188,17 @@ export default function SessionPage() { } }, []); + const fetchSidebarSessions = useCallback(async () => { + try { + const res = await fetch("/api/sessions"); + if (!res.ok) return; + const body = (await res.json()) as { sessions?: DashboardSession[] } | null; + setSidebarSessions(body?.sessions ?? []); + } catch { + // non-critical + } + }, []); + useEffect(() => { if (!sessionIsOrchestrator) { setZoneCounts(null); @@ -194,19 +208,21 @@ export default function SessionPage() { // Initial fetch — session first, zone counts after (avoids blocking on slow /api/sessions) useEffect(() => { fetchSession(); + fetchSidebarSessions(); // Delay zone counts so the heavy /api/sessions call doesn't contend with session load const t = setTimeout(fetchProjectSessions, 2000); return () => clearTimeout(t); - }, [fetchSession, fetchProjectSessions]); + }, [fetchSession, fetchProjectSessions, fetchSidebarSessions]); // Poll every 5s useEffect(() => { const interval = setInterval(() => { fetchSession(); fetchProjectSessions(); + fetchSidebarSessions(); }, 5000); return () => clearInterval(interval); - }, [fetchSession, fetchProjectSessions]); + }, [fetchSession, fetchProjectSessions, fetchSidebarSessions]); if (loading) { return ( @@ -235,6 +251,8 @@ export default function SessionPage() { isOrchestrator={sessionIsOrchestrator} orchestratorZones={zoneCounts ?? undefined} projectOrchestratorId={projectOrchestratorId} + projects={projects} + sidebarSessions={sidebarSessions} /> ); } diff --git a/packages/web/src/components/ActivityDot.tsx b/packages/web/src/components/ActivityDot.tsx index e60725682..7f638a9d8 100644 --- a/packages/web/src/components/ActivityDot.tsx +++ b/packages/web/src/components/ActivityDot.tsx @@ -2,22 +2,13 @@ import { cn } from "@/lib/cn"; -const activityConfig: Record< - string, - { label: string; dot: string; bg: string; text: string } -> = { - active: { label: "active", dot: "var(--color-status-working)", bg: "rgba(34,197,94,0.1)", text: "var(--color-status-working)" }, - ready: { label: "ready", dot: "var(--color-status-ready)", bg: "rgba(91,126,248,0.1)", text: "var(--color-status-ready)" }, - idle: { label: "idle", dot: "var(--color-status-idle)", bg: "rgba(72,79,88,0.25)", text: "var(--color-text-secondary)" }, - waiting_input: { label: "waiting", dot: "var(--color-status-attention)", bg: "rgba(210,153,34,0.12)", text: "var(--color-status-attention)" }, - blocked: { label: "blocked", dot: "var(--color-status-error)", bg: "rgba(248,81,73,0.1)", text: "var(--color-status-error)" }, - exited: { label: "exited", dot: "var(--color-status-done)", bg: "rgba(48,54,61,0.5)", text: "var(--color-text-muted)" }, -}; - -const fallbackConfig = { - dot: "var(--color-text-tertiary)", - bg: "rgba(74,74,74,0.2)", - text: "var(--color-text-muted)", +const activityLabels: Record = { + active: "active", + ready: "ready", + idle: "idle", + waiting_input: "waiting", + blocked: "blocked", + exited: "exited", }; interface ActivityDotProps { @@ -28,32 +19,31 @@ interface ActivityDotProps { } export function ActivityDot({ activity, dotOnly = false, size = 6 }: ActivityDotProps) { - const c = (activity !== null && activityConfig[activity]) || { - label: activity ?? "unknown", - ...fallbackConfig, - }; + const label = (activity !== null && activityLabels[activity]) || activity || "unknown"; + const dataActivity = activity ?? undefined; const isPulsing = activity === "active"; if (dotOnly) { return (
); } return ( - - {c.label} + + {label} ); diff --git a/packages/web/src/components/AttentionZone.tsx b/packages/web/src/components/AttentionZone.tsx index 0ae427fd4..0ea5302df 100644 --- a/packages/web/src/components/AttentionZone.tsx +++ b/packages/web/src/components/AttentionZone.tsx @@ -32,39 +32,32 @@ const zoneConfig: Record< AttentionLevel, { label: string; - color: string; - caption: string; + emptyMessage: string; } > = { merge: { label: "Ready", - color: "var(--color-status-ready)", - caption: "Cleared to land", + emptyMessage: "Nothing cleared to land yet.", }, respond: { label: "Respond", - color: "var(--color-status-error)", - caption: "Human judgment needed", + emptyMessage: "No agents need your input.", }, review: { label: "Review", - color: "var(--color-accent-orange)", - caption: "Code waiting on eyes", + emptyMessage: "No code waiting for review.", }, pending: { label: "Pending", - color: "var(--color-status-attention)", - caption: "Waiting on external state", + emptyMessage: "Nothing blocked.", }, working: { label: "Working", - color: "var(--color-status-working)", - caption: "Agents are actively moving", + emptyMessage: "No agents running.", }, done: { label: "Done", - color: "var(--color-text-tertiary)", - caption: "Completed or exited", + emptyMessage: "No completed sessions.", }, }; @@ -118,14 +111,15 @@ function AttentionZoneView({ className="accordion-header" onClick={() => onToggle(level)} aria-expanded={!collapsed} + aria-controls={`accordion-body-${level}`} > - + {config.label} {sessions.length} -
+
{sessions.length > 0 ? (
{visibleSessions.map((session) => @@ -159,7 +153,7 @@ function AttentionZoneView({
) : compactMobile ? (
-
No sessions
+
{config.emptyMessage}
) : null}
@@ -171,16 +165,15 @@ function AttentionZoneView({
-
+
{config.label} {sessions.length}
-

{config.caption}

{sessions.length > 0 ? ( -
+
{sessions.map((session) => ( ))}
- ) : ( -
- No sessions -
- )} + ) : null}
); @@ -247,7 +236,7 @@ function MobileSessionRow({