Files
FraterCCCLXIII e1f5a6662f design: cool-grey palette, runtime theme switcher, and token-system cleanup (#458)
* fix(conversation): cap chat column width at 800px

Replace responsive max-w-4xl / max-w-6xl with max-w-[800px] so the
middle column stays narrower on large viewports.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(chat): Connect Repo CTA and hide empty branch pill

- Use COMMON$CONNECT_REPO with FolderOpen when no repo/workspace is linked
- Show branch control only when selectedBranch is set (drop No Branch)
- Cap chat interface wrapper at max-w-[800px] without right-panel width coupling

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): refine input controls and local auth fallback

Improve chat input pills and model dropdown interactions while ensuring local agent-server auth uses the configured session key for default-local and cloud-proxy calls to avoid stale-key 401s.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): align attachment and placeholder control styling

Move the file-attach trigger into the chat action controls so it sits before Tools, and restyle it as a grey plus button with a circular hover state to match adjacent controls. Also align the chat input placeholder color with the same neutral control tone for visual consistency.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): simplify agent status labels and tone

Shorten English agent-status messages for the chat pill and align the status text color with the other grey controls for a more consistent compact UI.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): align model popover settings row styling

Add an LLM Settings action to the model popover and normalize its layout, spacing, and divider treatment to match existing dropdown menu patterns while keeping left-aligned positioning.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): restyle status controls and move send action

Make the agent-status control transparent by default with gray-to-white icon hover behavior, and move the submit button to the bottom-right controls area beside agent status.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): tighten spacing above git control bar

Reduce the top margin before the git control bar so it better matches the bottom spacing around the chat action controls.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): gate submit button on input content

Keep the send button inactive until the input has non-whitespace text, and align the revised button sizing/positioning with the bottom action row layout.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): streamline overlays and remove legacy event rails

Unify chat control styling and overlay behavior so status/typing/scroll controls float above the thread without adding layout bars, and remove left-rail/checkmark affordances from grouped and generic event cards for a cleaner stream.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): tighten status indicator spacing

Reduce status indicator pill padding and icon size, and add right text padding to balance the compact layout in the chat control overlay.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): prioritize centered scroll control over loader

Keep the scroll-to-bottom control centered and visible whenever the user is away from the bottom, and use solid base/hover fills so it matches the updated chat surface styling.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): soften conversation event header styling

Use the lighter gray chat tone for conversation event header labels/icons and switch those labels to normal weight so grouped event rows match the updated control styling.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): refine markdown spacing and divider styling

Tighten markdown vertical rhythm in chat content, add a shared grey horizontal-rule renderer, and tune heading hierarchy to medium/compact styles for clearer structure without heavy emphasis.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): tighten vertical spacing in action event rows

Reduce stacked margins and paddings across grouped action rows, generic event cards, and collapsible thinking blocks so adjacent conversation entries read as a denser, more consistent stream.

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs(design): add app gray palette reference artifacts

Capture the current gray color usage in dedicated SVG references, including both a curated palette and a strict exhaustive inventory for design and UI consistency work.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): align compact input overflow menus with menu conventions

Keep add-file pinned inline, collapse controls only when width truly runs out, and switch overflow entries to standard context-menu row/submenu patterns while preserving the send button layout at tight widths.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(conversation-panel): use list filter icon for older filters

Swap the older-conversations summary toggle icon to ListFilter so it matches the intended sidebar filter affordance.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): complete local workspace launch flow in git controls

Switch the local git control CTA from repository connection to workspace launching, including an above-button workspace menu and automatic add-workspace modal when none exist. This also captures the pending chat action/menu styling and test updates in the current working tree.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): relocate desktop vertical padding to input controls

Remove desktop top/bottom padding from the main chat panel and apply equivalent bottom spacing to the chat control area so the open repo/workspace controls and input footer keep consistent breathing room.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(conversation): remove bottom margin from chat pane header

Drop the chat header bottom margin so the conversation title row sits flush with the content below.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(chat): refresh git control bar immediately after Connect Repo

The "Connect Repo" empty-state in the chat input footer kept rendering
even after the Open Repository modal had successfully launched a clone
and the agent had reported the repository as ready. The bar would only
heal after a hard refresh (or never, on cloud backends).

Three independent bugs were stacking:

1. Optimistic update was writing to the wrong React Query cache key.
   `useUpdateConversationRepository.onMutate` called `setQueryData`
   with `["user", "conversation", id]` (3 elements), but
   `useUserConversation` reads from
   `["user", "conversation", id, backendId, orgId]` (5 elements).
   `setQueryData` requires an *exact* key match, so the update landed
   on an orphan cache entry that no observer ever read. Switched to
   `setQueriesData`/`getQueriesData` with the 3-element prefix so the
   optimistic write actually reaches the active query (Tanstack v5
   prefix-matches `setQueriesData` filters). Also normalized
   `branch`/`gitProvider` to `null` to match the shape produced by the
   server-side refetch and prevent identity-flicker between the two
   updates.

2. Cloud `batchGetCloudConversations` / `searchCloudConversations`
   ignored the local repo selection entirely. For local backends
   `toAppConversation` overlays `selected_repository`/`selected_branch`/
   `git_provider` from `localStorage`, but the cloud path returned the
   raw SaaS payload — and the SaaS often returns `null` for those
   fields until its own background hydration finishes. So every
   refetch (mutation invalidation, 30s poll, panel mount) overwrote
   the optimistic value with `null` and the bar snapped back to
   "Connect Repo". Added `overlayStoredRepoSelection` which fills only
   the `null` slots from local storage; populated server values still
   win, so we don't shadow real backend changes.

3. `updateConversationRepository` overwrote the entire metadata blob.
   `setStoredConversationMetadata` is replace-not-merge, so calling it
   with just `{selected_repository, selected_branch, git_provider}`
   silently dropped `selected_workspace` (the local-folder attach
   marker used by the Files tab to default to diff view, see the
   "Files tab diff-view default logic" note in `AGENTS.md`). Now reads
   the existing entry first and spreads it under the new repo fields.

Defense-in-depth changes:

- `useLocalGitInfo` now stays enabled until the conversation reports a
  *complete* repo tuple (`selected_repository` + `git_provider` +
  `selected_branch`), not just `selected_repository`. This lets the
  bar recover from partial-metadata cases (e.g. cloud hydration
  populates only the repo name first, or the user clones into a
  subdirectory of `working_dir`). The probe also gained a nested
  `find . -mindepth 2 -maxdepth 4 -name .git` fallback so a clone
  into `<workingDir>/<repo>/` is still detected after the direct
  `git remote get-url origin` in `<workingDir>` returns "no such
  remote 'origin'" (the agent-server pre-initialises every workspace
  as a worktree, so the parent directory always has a `.git` folder
  with no remote).

- `useUpdateConversationRepository.onSettled` invalidates
  `["local-git-info", conversationId]` so the bar re-probes
  immediately after a connect rather than waiting on the next 10s
  refetch tick.

- `git-control-bar.tsx`'s `hasRepository` predicate now keys off the
  *resolved* `selectedRepository` + `gitProvider` (which include the
  local-git probe's findings), not just the conversation field. This
  lets pull/push/PR buttons light up for local-workspace conversations
  whose repo metadata was inferred from `git remote`, matching what
  the repo + branch chips already showed.

Verification

I traced the failure mode by hitting the live agent-server directly:

  $ curl -s -X POST .../api/bash/execute_bash_command \\
      -H "X-Session-API-Key: \$KEY" \\
      -d '{"command":"git remote get-url origin", "cwd":"<workingDir>"}'

  git remote: error: No such remote 'origin'
  git rev-parse HEAD: ambiguous argument 'HEAD': unknown revision

confirming the worktree-without-remote shape that broke the direct
probe and forced the nested-find fallback.

Tests

  __tests__/hooks/mutation/use-update-conversation-repository.test.tsx
    - optimistically updates the cached conversation under the
      prefix-extended key used by useUserConversation
    - rolls back the prefix-keyed cache entry when the mutation rejects

  __tests__/api/cloud-conversation-service.test.ts (new)
    - overlays locally-stored repo selection onto
      batchGetCloudConversations results when the server returns nulls
    - prefers the cloud server values over locally-stored selections
      when present
    - leaves null entries untouched when the cloud server returns null
      for a missing conversation
    - returns an empty array without calling the proxy when no ids
      are provided
    - overlays repo selection on each item returned from
      searchCloudConversations

Wider sweep:

  npx vitest run __tests__/hooks/mutation \\
                __tests__/api/cloud-conversation-service.test.ts \\
                __tests__/api/conversation-metadata-store.test.ts \\
                __tests__/api/agent-server-adapter.test.ts \\
                __tests__/components/features/chat
  -> 22 files, 152 tests passed.

User-visible behavior after this change:

  1. Clicking Launch in the Connect Repo modal flips the bar to
     repo + branch chips immediately (optimistic update now reaches
     the active query).
  2. The bar stays flipped through the next refetch on cloud
     backends (overlay keeps the local selection visible until the
     SaaS catches up).
  3. Bar picks up nested clones within ~1s on local backends
     (local-git-info invalidation forces a re-probe instead of
     waiting on the 10s poll), and the nested-find fallback handles
     'clone into <workingDir>/<repo>/' flows.
  4. Pull/push/PR buttons now light up for local-workspace
     conversations whose remote was inferred from git remote.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(conversation): make right-panel drawer state session-only

The right-side drawer's open/closed state (`isRightPanelShown` /
`hasRightPanelToggled`) was persisted in localStorage, which made
the panel feel sticky in a way users didn't expect — it would still
be open after reloads or revisits even though they wanted a clean,
focused chat view.

Move drawer state fully into the in-memory Zustand store so it:
- always starts closed on app load (or on opening a conversation
  after a restart),
- survives in-app navigation because Zustand stays alive across
  React Router transitions,
- only persists tab selection (`selectedTab`), which is the part
  users do want to come back to.

The legacy `rightPanelShown` field is silently stripped from older
persisted blobs by `sanitizeStoredState`, so old localStorage data
doesn't churn or leak into the new schema.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(ui): standardize three-dots ellipsis trigger across the app

Different surfaces had drifted to slightly different "more options"
buttons:
- conversation header used a 24x24 icon with a hardcoded fill color,
- conversation cards in the side panel used a separate square
  `ellipsis.svg` glyph,
- the conversation tab bar used a 20x20 icon with bespoke colors,
- LLM profile rows wrapped the icon in a bordered button with
  yet another color.

Promote `EllipsisButton` to be the canonical trigger and route every
inline variant through it so size (w-4 h-4 / 16x16), color
(`text-[#9299AA]`), and hover treatment (`hover:text-white
hover:bg-white/10`) stay consistent everywhere. Layout-only overrides
(e.g. translate, opacity-when-paused) flow through `className`, and a
`testId` escape hatch keeps the existing `profile-menu-trigger`
selector working.

The chat-input overflow button intentionally keeps its pill-shaped
custom variant; a doc comment on `EllipsisButton` calls that out so
future contributors don't replace it.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore(design): add 15-stop cool grey palette and complete migration plan

Documents migration of ~98 scattered grey values across agent-canvas to a
unified 15-stop cool blue-grey family (hue ≈ 220–224°), with all existing
hex values, Tailwind utilities, CSS variables, and alpha variants mapped to
the nearest new token by RGB + lightness proximity.

Artifacts:
- cool-grey-palette.svg: visual palette strip + per-shade migration map
- cool-grey-migration.md: CSS/Tailwind definitions, per-file migration
  tables, alpha variant equivalents, and a 6-phase implementation plan

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): remove circular pill and ripple effect from autocomplete caret buttons

Replace the default HeroUI selector button styling (rounded-full, fixed dimensions,
hover fill) with a flat transparent icon and disable the press ripple via
selectorButtonProps={{ disableRipple: true }} on all Autocomplete instances.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(design): enforce single border color token across all UI elements

- Unify --oh-border-input to cool-grey-700 (same as --oh-border), eliminating
  the 3-way border split across inputs, cards, and dividers
- Replace border-neutral-600 in .button-base with border-[var(--oh-border)]
- Replace all border-tertiary usages (27 files) with --oh-border for outer
  borders and --oh-border-subtle for within-panel dividers
- Fix border-tertiary-light on toggle switch OFF state → --oh-border
- Fix border-t-tertiary on app-settings Git section divider → --oh-border-subtle
- Fix divide-tertiary in profiles-body → divide-[var(--oh-border-subtle)]
- Fix secrets table row dividers: --oh-border-subtle → --oh-border
- Fix files-tab toolbar and file-quick-row header lines → --oh-border
- Fix repo-connector and new-conversation card borders → --oh-border
- Remove bg-surface from automations-list and automation-detail routes so
  they inherit bg-base from the root layout, matching all other pages

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(design): migrate automations to shared tokens; fix secondary button, card, and hover strokes

- Replace all legacy tailwind.config.js color tokens in automations/ (27 files):
  bg-surface-card → bg-[var(--oh-surface)], bg-surface-elevated → bg-surface-raised,
  border-border → border-[var(--oh-border)], text-content-muted → text-muted,
  status/toggle/badge tokens → --oh-success/--oh-danger/--oh-muted variants
- Fix active-status-badge and status-badge inactive fills from bg-border → bg-surface-raised
- BrandButton secondary variant: yellow outline+text → border-[var(--oh-border)] text-white
  hover:bg-surface-raised; move hover:opacity-80 off base onto primary/tertiary only
- BrandButton primary: replace text-base (font-size conflict) with text-[var(--oh-color-base)]
  so all variants share the base text-sm font size
- Card primitive default/outlined themes: --oh-border-input → --oh-border (fixes visible
  mismatch between repo-connector and Start from Scratch cards on home screen)
- marketplace-card, skill-card, skills-toolbar: replace hover:border-white/40 with
  hover:border-[var(--cool-grey-500)] and focus:ring-primary/60 with focus:ring-[var(--oh-border)]
- automations routes: remove explicit bg-surface so pages inherit bg-base from root layout

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(design): normalize spinners, borders, and accent colors to design tokens

Replace hardcoded `border-primary`, `border-blue-500`, and `text-primary`
with cool-grey-aligned tokens (`border-white`, `border-white/20`,
`var(--oh-border)`, `var(--oh-muted)`) across loading spinners, modals,
dropdowns, and link styles. Switch dropdown selected-item highlight from
`--oh-interactive-active` to `--oh-interactive-selected`.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat(theme): add runtime color theme switcher with OpenHands-Neutral palette

- Add src/themes/color-themes.ts: two themes (OpenHands-DeepSea /
  OpenHands-Neutral) with --cool-grey-* scale overrides and matching
  --heroui-* HSL channel overrides (default, content, background,
  foreground families) so HeroUI components and portalled popovers
  both respond to theme changes.
- Inject overrides via a <style> tag on [data-agent-server-ui] AND
  [data-theme=dark] so portal content rendered to document.body picks
  up the new palette alongside inline components.
- Add ThemeInput (SettingsDropdownInput) to Application Settings;
  applies the theme immediately on selection and persists to
  localStorage under openhands-color-theme.
- Add ColorThemeApplier to root.tsx so the persisted theme is
  re-applied on every page load with no flash.

Additional token fixes found during theme testing:
- Define --color-tertiary-alt → --oh-text-dim in tailwind.css so the
  ~25 placeholder:text-tertiary-alt / text-tertiary-alt usages
  (API key input, helper text, badges) resolve correctly.
- Fix environment-switch-overlay: replace bg-card / border-border /
  text-foreground with --oh-surface / --oh-border / --oh-foreground.
- Fix AutocompleteSection headings in model-selector: add
  classNames={{ heading: "text-[var(--oh-muted)]" }} so Verified /
  Other Models labels are readable against the dropdown background.
- Unify structural panel dividers: sidebar right edge + footer
  separator + files-tab tree divider all changed from
  --oh-border-subtle to --oh-border, matching the right panel.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(ui): improve suggestion card hover to match secondary button style

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(theme): set OpenHands-Neutral as the default color theme

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: failing tests

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: hieptl <hieptl.developer@gmail.com>
2026-05-16 00:57:13 +07:00

66 lines
2.3 KiB
TypeScript

import { heroui } from "@heroui/react";
export default heroui({
defaultTheme: "dark",
layout: {
radius: {
small: "5px",
large: "20px",
},
},
themes: {
dark: {
colors: {
primary: "#4465DB",
// Map HeroUI's zinc-based semantic colours to our cool-grey palette.
// This ensures every HeroUI component that uses bg-default, bg-content*,
// etc. stays within the same colour family as the rest of the UI.
background: {
DEFAULT: "#0B0E14", // cool-grey-950 — app shell base
foreground: "#F7F9FC", // cool-grey-50
},
foreground: {
DEFAULT: "#C3CDDC", // cool-grey-300 — primary readable text
"50": "#05070A", // cool-grey-975
"100": "#0B0E14", // cool-grey-950
"200": "#21252F", // cool-grey-925
"300": "#2C313F", // cool-grey-900
"400": "#383F50", // cool-grey-800
"500": "#4B5468", // cool-grey-700
"600": "#626D82", // cool-grey-600
"700": "#7E8A9E", // cool-grey-500
"800": "#A3B0C4", // cool-grey-400
"900": "#C3CDDC", // cool-grey-300
},
// Surface layers: panel → card → inner card → inset
content1: { DEFAULT: "#21252F", foreground: "#EEF2F7" }, // cool-grey-925 / 100
content2: { DEFAULT: "#2C313F", foreground: "#DCE3EE" }, // cool-grey-900 / 200
content3: { DEFAULT: "#383F50", foreground: "#C3CDDC" }, // cool-grey-800 / 300
content4: { DEFAULT: "#4B5468", foreground: "#A3B0C4" }, // cool-grey-700 / 400
focus: {
DEFAULT: "#ffffff", // white focus ring — visible on all dark surfaces
},
default: {
"50": "#05070A", // cool-grey-975
"100": "#0B0E14", // cool-grey-950
"200": "#21252F", // cool-grey-925
"300": "#2C313F", // cool-grey-900
"400": "#383F50", // cool-grey-800
"500": "#4B5468", // cool-grey-700
"600": "#626D82", // cool-grey-600
"700": "#7E8A9E", // cool-grey-500
"800": "#A3B0C4", // cool-grey-400
"900": "#C3CDDC", // cool-grey-300
DEFAULT: "#383F50", // cool-grey-800 — hover/selected tint
foreground: "#F7F9FC", // cool-grey-50 — text on default bg
},
},
},
},
});