mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
* feat(desktop): hover-reveal collapsed chat sidebar as a fixed overlay
When the sessions sidebar is collapsed, hovering the left edge now floats
it back in as a fixed overlay over the main content instead of just being
hidden. The collapsed grid track stays at 0px so the panel never reserves
space — it slides over whatever's underneath and retracts on pointer-leave.
- PaneShell: new hoverReveal prop. When a pane is collapsed + hoverReveal,
render an edge hot-zone + a side-anchored floating panel (absolute, full
height, honors any persisted resize width) that slides in on hover/focus.
- ChatSidebar: force the (otherwise opacity-0 when collapsed) sidebar fully
visible + interactive while the overlay is revealed, via an
in-data-[pane-hover-reveal=open] variant.
- desktop-controller: opt the chat-sidebar pane into hoverReveal.
* feat(desktop): lower window minWidth 900→400
Lets the window shrink to a narrow rail (e.g. for the collapsed
hover-reveal sidebar) instead of being floored at 900px.
* fix(desktop): render full sidebar content in hover-reveal overlay
The hover-reveal overlay showed only the nav rail — session rows, search,
pinned/recents were gated behind `sidebarOpen` (false while collapsed), so
they never mounted in the floated panel.
Add a $sidebarRevealed store the PaneShell overlay drives via a new
onHoverRevealChange callback, and gate ChatSidebar's content on
`sidebarOpen || sidebarRevealed` (contentVisible) instead of raw open
state. The overlay now shows the complete sidebar.
* fix(desktop): drop shadow on hover-reveal sidebar overlay
* feat(desktop): hover-reveal the file-browser sidebar too
The reveal mechanism already lives in the shared Pane primitive — the
right rail just opts in with hoverReveal. Its content renders
unconditionally, so (unlike the chat sidebar) it needs no extra
content-visibility gating.
* clean(desktop): tighten hover-reveal pane code
KISS pass — flatten the translate ternary, derive a single `revealed`,
inline the edge style, drop the redundant set-guard, and trim comments to
the house one-liner style. No behavior change.
* fix(desktop): stop hiding sidebar nav labels on narrow windows
The nav labels (New session, Skills, …) and the ⌘N hint were gated on a
viewport breakpoint (max-[46.25rem]:hidden), so shrinking the window hid
them even when the sidebar itself was wide — including in the hover-reveal
overlay. Drop the gate; the label already truncates (min-w-0 flex-1) so it
ellipsizes gracefully in a narrow rail, and contentVisible already hides it
when collapsed to the icon rail.
* feat(desktop): auto-collapse both sidebars below 600px into hover-reveal
Add a Pane `forceCollapsed` prop — collapses the track without writing to
the store (so the saved open state restores when the window widens) while
keeping hoverReveal alive (unlike `disabled`, which suppresses it).
desktop-controller watches (max-width: 600px) and force-collapses the chat
sidebar + file browser, so on a narrow window both rails get out of the way
and the hover-reveal overlay becomes the way in.
* feat(desktop): hover-intent + refined easing for sidebar reveal
- Gate the reveal on pointer velocity: the full-height edge hot-zone now
only arms on a slow, deliberate pass (<=0.55 px/ms). Fast sweeps toward
the titlebar/statusbar — or off the window — blow past the threshold and
never trigger, so the wide hit area stops being a nuisance.
- Swap the slide easing to cubic-bezier(0.32,0.72,0,1) at 260ms (snappy-out,
soft-land) for a more serious-app feel.
* fix(desktop): don't reveal sidebar during window resize
Resizing the window parks the cursor on the screen edge and fires slow
pointermoves over the hot-zone, reading as deliberate intent. Guard the
reveal on (a) e.buttons !== 0 — any button-held drag, incl. edge-resize —
and (b) a 250ms cooldown after any window resize event.
* feat(desktop): hoverIntent-style poll gate + inert contents during slide
Replace the single-sample velocity check (too eager — fired on any one slow
move, incl. resize drift) with a port of Brian Cherne's hoverIntent: poll
the pointer every 90ms and only arm once it has *settled* (moved <5px between
two consecutive polls inside the edge zone). Fly-bys, pass-throughs, and
resize drift never produce two close samples in a row, so they don't trigger.
Also keep the revealed panel's CONTENTS pointer-events-none until the slide-in
transition finishes (onTransitionEnd → settled), so you can't misclick a
session row mid-animation. Resets on retract.
* fix(desktop): no cursor/hit-test leak before reveal settles
The edge hot-zone showed cursor:pointer the instant the pointer touched it —
before the panel was armed or in view. And contents were inert but the panel
itself still hit-tested, so the cursor could flip mid-slide. Fix: hot-zone is
cursor-default (it's invisible), and the whole panel is pointer-events-none
until revealed && settled, so the cursor never changes or lands on a row
before the slide-in finishes.
* fix(desktop): geometry-driven close so revealed panel always retracts
The revealed panel relied on its own onPointerLeave to close — but a panel
that slid in under a still cursor (or whose contents were inert during the
slide) never fires enter/leave, so it got stuck open (esp. the file browser).
onTransitionEnd also bubbled from the file-tree's own row transitions,
tripping the settled flag wrongly.
Replace with a document-level pointermove watcher that closes once the cursor
leaves the panel's bounding rect + a 24px grace — independent of pointer-events
state or what the contents do. Gate interactivity on a simple slide-duration
timer (interactive) instead of the fragile transitionEnd, so the cursor still
can't flip or land on a row before the panel is in view.
* feat(desktop): make sidebar toggle shortcuts reveal when force-collapsed
mod+b / mod+j were no-ops on a narrow (force-collapsed) window — they
flipped the store but the pane ignores it. Now the toggle handlers also
dispatch PANE_TOGGLE_REVEAL_EVENT; a force-collapsed Pane listens (only while
overlayActive) and flips its hover-reveal, so the shortcut floats the rail in
(and back out) at this responsive breakpoint.
* refactor(desktop): name the 600px sidebar collapse breakpoint
Hoist the inline '(max-width: 600px)' literal into
SIDEBAR_COLLAPSE_BREAKPOINT_PX + SIDEBAR_COLLAPSE_MEDIA_QUERY in
layout-constants, so the responsive collapse point is a single named source
of truth instead of a magic string in the controller.
* tweak(desktop): sidebar auto-collapse breakpoint 600px -> 768px
768 is the standard md breakpoint and a more honest 'no room to dock' point.
* tweak(desktop): halve sidebar reveal slide duration 260ms -> 130ms
* Revert "tweak(desktop): halve sidebar reveal slide duration 260ms -> 130ms"
This reverts commit 6009a13200.
* perf(desktop): pre-mount hover-reveal contents to kill slide-in stall
The reveal mounted the (heavy, virtualized) sidebar contents in the same
frame the slide started, so the browser stalled painting the transform until
the mount finished — a ~100-200ms beat before the panel moved, very visible
on the instant keyboard toggle (hover masked it via the 90ms intent poll).
Report overlayActive (collapsed-overlay mode) rather than the live reveal
state to the mount consumer, so contents stay mounted off-screen while
collapsed and reveal is a pure transform. Visibility is still driven
separately by the data-pane-hover-reveal attr + the slide transform.
* fix(desktop): make reveal hotkey spammable
Two throttles on the reveal toggle:
- The handler fired both the reveal event AND toggleSidebarOpen() per press;
the store write hits localStorage synchronously every keystroke + recomputes
the grid, janking rapid presses. When collapsed, only dispatch the reveal
event (the store toggle was a no-op anyway).
- The geometry close-watcher slammed a keyboard-opened panel shut on the first
stray pointermove (trackpad jitter), fighting hotkey spam. Keyboard reveals
now ignore geometry until the cursor actually enters the panel, then the
mouse takes over.
* fix(desktop): inset reveal hot-zone past the OS window-resize gutter
The hot-zone sat flush at the window edge (left-0/right-0), overlapping the
OS resize grab strip — reaching to drag-resize naturally slows the cursor
there, which hoverIntent reads as settled and reveals before the resize drag
even starts. Inset the hot-zone 8px so the outermost edge stays a pure
resize/drag region and only an intentful move just inside it arms a reveal.
* fix(desktop): keep reveal hot-zone at edge, gate arming past resize gutter
Insetting the hot-zone made it unreachable when moving fast. Instead, anchor
the zone flush at the edge (w-4, always captures the pointer) but only ARM the
reveal when the cursor settles >=8px in from the edge — so a resize-reach that
parks on the outermost OS grab strip never triggers, while a deliberate move
into the zone still does. Keeps polling while in the gutter so moving inward
still arms.
* refactor(desktop): rebuild hover-reveal as pure CSS, delete the JS state machine
The hand-rolled pointer state machine (hoverIntent poll, refs, timers, document
pointermove geometry-close, interactive gate, resize cooldowns, keyboard-held
suppression) was fragile and side/instance-specific — hover broke on the right
rail, keyboard toggles triggered phantom animations, resize popped it open.
Replace all of it with the native primitive: CSS group-hover drives the slide
transform; a transition-delay on enter (instant on leave) is the hover-intent
gate (a fast pass-by doesn't dwell long enough to open); a thin edge trigger
inset past the OS resize grab strip arms it; and a single `forced` bool
(data-forced, toggled by the keyboard event) pins it open. Side-agnostic by
construction — group-hover doesn't care which edge or which pane.
Net: ~200 lines of imperative pointer logic → ~40 lines of declarative CSS.
* fix(desktop): don't animate hover-reveal panel across viewport on side flip
Flipping panes changed the off-screen transform from -translateX (off the
left) to +translateX (off the right). transition-transform interpolated
between them, passing through translate-x-0 (fully on-screen) mid-way — so the
hidden panel visibly slid across the window to reach its new hiding spot.
Key the panel on side so it remounts off-screen on the new edge with no
transition to play.
* clean(desktop): tighten hover-reveal markup
KISS pass on the CSS-driven reveal: reuse the existing `side` instead of a
local `left`, move the static duration/ease to inline style (drop two
single-use CSS vars + their arbitrary-value classes, keep only the
state-dependent enter-delay var), and trim comments to the house one-liner
density. No behavior change.
* fix(desktop): inset titlebar past traffic lights when sidebar is force-collapsed
The titlebar content inset (clearing the macOS traffic lights) keyed off the
stored sidebarOpen/fileBrowserOpen, but below the collapse breakpoint both
rails are force-collapsed so the left edge is uncovered while the store still
says open — content (the intro wordmark) overflowed under the lights. Gate
leftEdgePaneOpen on !narrowViewport using the shared SIDEBAR_COLLAPSE_MEDIA_QUERY.
Also rename the now-misleading reveal plumbing to match what it actually does:
onHoverRevealChange -> onOverlayActiveChange, $sidebarRevealed ->
$sidebarOverlayMounted (+ setter/consumer). It reports/stores collapsed-overlay
mode (mount gate), not live reveal state.
* feat(desktop): small --nous-shadow lift on revealed hover-reveal panels
Add a --nous-shadow token (white-based on light, black-based on dark) and apply
it to the floating sidebar panel only while revealed (group-hover / data-forced)
so it reads as lifted off the surface. No shadow on the off-screen panel.
* feat(desktop): shadow-reveal lift on revealed hover-reveal panels
Mirror the --shadow-nous layered falloff into a new --shadow-reveal token whose
drop color flips per mode (white on light, black on dark) via --shadow-reveal-raw
set in :root / :root.dark. Apply the generated shadow-reveal utility to the
floated panel only while revealed (group-hover / data-forced). Leaves the shared
--shadow-nous untouched.
* feat(desktop): use tuned reveal shadow, drop per-mode token
Replace the --shadow-reveal token machinery with Brooklyn's tuned literal
(0 -18px 18px -5px #0000003b) inline per-panel via --reveal-shadow, y-offset
sign flipped for the right side. Same color both modes. Reverts styles.css to
pristine (token removed).
* fix(desktop): use the reveal shadow verbatim, don't invert it per side
Flipping the y-offset sign for the right side inverted the shadow's direction
(cast-up -> cast-down), making it read heavier — not a mirror. The mirror axis
for a left/right panel is offset-x, which is 0 here, so both sides take the
tuned value as-is: 0 -18px 18px -5px #0000003b.
* clean(desktop): hoist reveal shadow to a named const
Move the inline reveal-shadow literal to HOVER_REVEAL_SHADOW alongside the
other HOVER_REVEAL_* tuning consts; drop the now-stale per-side comment.
* fix(desktop): truncate titlebar title before the right tool cluster
The session title used a hardcoded max-w-[52vw] that's blind to where the
right-side tools start, so it ran under them at narrow widths / with pane
tools present. Bound the title container by the same vars the titlebar drag
region uses (--titlebar-content-inset + --titlebar-tools-right +
--titlebar-tools-width) so it truncates exactly at the cluster's left edge.
* fix(desktop): responsive markdown tables — floor width + nowrap headers
The wrapper had overflow-x-auto but the table was w-full with auto layout, so
instead of scrolling it crushed columns until even header words broke mid-word
(Tim/e, Nig/ht). Add a min-w-[18rem] floor so it scrolls horizontally when the
column is narrower than readable, and whitespace-nowrap on th so headers never
break mid-word. Above the floor it still wraps cells naturally.
* fix intro
1225 lines
40 KiB
CSS
1225 lines
40 KiB
CSS
@import 'tailwindcss';
|
|
@plugin '@tailwindcss/typography';
|
|
@import 'tw-shimmer';
|
|
@import 'katex/dist/katex.min.css';
|
|
@import '@vscode/codicons/dist/codicon.css';
|
|
@custom-variant dark (&:is(.dark *));
|
|
|
|
@font-face {
|
|
font-family: 'Collapse';
|
|
font-style: normal;
|
|
font-weight: 700;
|
|
font-display: swap;
|
|
src: url('../../../node_modules/@nous-research/ui/dist/fonts/Collapse-Bold.woff2') format('woff2');
|
|
}
|
|
|
|
@theme inline {
|
|
--color-background: var(--dt-background);
|
|
--color-foreground: var(--dt-foreground);
|
|
--color-card: var(--dt-card);
|
|
--color-card-foreground: var(--dt-card-foreground);
|
|
--color-muted: var(--dt-muted);
|
|
--color-muted-foreground: var(--dt-muted-foreground);
|
|
--color-popover: var(--dt-popover);
|
|
--color-popover-foreground: var(--dt-popover-foreground);
|
|
--color-primary: var(--dt-primary);
|
|
--color-primary-foreground: var(--dt-primary-foreground);
|
|
--color-secondary: var(--dt-secondary);
|
|
--color-secondary-foreground: var(--dt-secondary-foreground);
|
|
--color-accent: var(--dt-accent);
|
|
--color-accent-foreground: var(--dt-accent-foreground);
|
|
--color-border: var(--dt-border);
|
|
--color-input: var(--dt-input);
|
|
--color-ring: var(--dt-ring);
|
|
--color-destructive: var(--dt-destructive);
|
|
--color-destructive-foreground: var(--dt-destructive-foreground);
|
|
|
|
--color-midground: var(--dt-midground);
|
|
--color-midground-foreground: var(--dt-midground-foreground);
|
|
|
|
--font-sans: var(--dt-font-sans);
|
|
--font-mono: var(--dt-font-mono);
|
|
|
|
--spacing-mul: var(--dt-spacing-mul, 1);
|
|
|
|
--radius-xs: calc(var(--radius-scalar) * 0.125rem);
|
|
--radius-sm: calc(var(--radius-scalar) * 0.5rem);
|
|
--radius-md: calc(var(--radius-scalar) * 0.625rem);
|
|
--radius-lg: calc(var(--radius-scalar) * 0.75rem);
|
|
--radius-xl: calc(var(--radius-scalar) * 1rem);
|
|
--radius-2xl: calc(var(--radius-scalar) * 1.5rem);
|
|
--radius-3xl: calc(var(--radius-scalar) * 2rem);
|
|
--radius-4xl: calc(var(--radius-scalar) * 2.5rem);
|
|
|
|
--color-sidebar-ring: var(--sidebar-ring);
|
|
--color-sidebar-border: var(--sidebar-border);
|
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
--color-sidebar-accent: var(--sidebar-accent);
|
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
--color-sidebar-primary: var(--sidebar-primary);
|
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
--color-sidebar: var(--sidebar);
|
|
|
|
--shadow-xs: 0 0.0625rem 0.125rem color-mix(in srgb, #000 5%, transparent);
|
|
--shadow-sm:
|
|
0 0 0 0.0625rem color-mix(in srgb, var(--dt-foreground) 6%, transparent),
|
|
0 0.125rem 0.5rem color-mix(in srgb, #000 4%, transparent);
|
|
--shadow-md:
|
|
0 0 0 0.0625rem color-mix(in srgb, var(--dt-foreground) 8%, transparent),
|
|
0 0.25rem 1rem color-mix(in srgb, #000 8%, transparent),
|
|
0 1rem 2rem -1.5rem color-mix(in srgb, #000 18%, transparent);
|
|
/* Soft floating shadow for borderless modals/overlays. Single top light
|
|
source: every layer is centered (x=0) and cast downward, with negative
|
|
spread that grows with the blur so each layer is pulled horizontally inward
|
|
— the shadow pools below the panel instead of bleeding out every side.
|
|
Layered (contact → ambient) for a smooth, natural falloff. */
|
|
--shadow-nous:
|
|
0 0.125rem 0.25rem -0.125rem color-mix(in srgb, #000 7%, transparent),
|
|
0 0.5rem 0.75rem -0.375rem color-mix(in srgb, #000 6%, transparent),
|
|
0 1.25rem 1.75rem -0.875rem color-mix(in srgb, #000 6%, transparent),
|
|
0 2.25rem 3rem -1.75rem color-mix(in srgb, #000 0%, transparent);
|
|
/* Hairline border paired with --shadow-nous on borderless overlays.
|
|
currentColor resolves per-element, so it adapts to text color/theme. */
|
|
--stroke-nous: color-mix(in srgb, currentColor 3%, transparent);
|
|
--shadow-lg:
|
|
inset 0 0.0625rem 0 color-mix(in srgb, #fff 28%, transparent),
|
|
0 0 0 0.0625rem color-mix(in srgb, var(--dt-foreground) 8%, transparent),
|
|
0 0.75rem 2rem color-mix(in srgb, #000 12%, transparent);
|
|
--shadow-composer: 0 0.0625rem 0.125rem color-mix(in srgb, #000 5%, transparent);
|
|
}
|
|
|
|
@layer base {
|
|
:root {
|
|
color-scheme: light;
|
|
|
|
--theme-foreground: #17171a;
|
|
--theme-primary: #0053fd;
|
|
--theme-secondary: color-mix(in srgb, #0053fd 7%, #ffffff);
|
|
--theme-accent-soft: color-mix(in srgb, #0053fd 10%, #ffffff);
|
|
--theme-midground: #0053fd;
|
|
--theme-warm: #cf806d;
|
|
--theme-background-seed: #f8faff;
|
|
--theme-sidebar-seed: #f3f7ff;
|
|
--theme-card-seed: #ffffff;
|
|
--theme-elevated-seed: #ffffff;
|
|
--theme-bubble-seed: color-mix(in srgb, #0053fd 6%, #ffffff);
|
|
--theme-neutral-chrome: #f3f3f3;
|
|
--theme-neutral-sidebar: #f3f3f3;
|
|
--theme-neutral-card: #fcfcfc;
|
|
--theme-mix-chrome: 92%;
|
|
--theme-mix-sidebar: 100%;
|
|
--theme-mix-card: 22%;
|
|
--theme-mix-elevated: 28%;
|
|
--theme-mix-bubble: 0%;
|
|
--theme-fill-primary-accent-mix: 16%;
|
|
--theme-fill-secondary-accent-mix: 11%;
|
|
--theme-fill-tertiary-accent-mix: 8%;
|
|
--theme-fill-quaternary-accent-mix: 5%;
|
|
--theme-fill-quinary-accent-mix: 3%;
|
|
--theme-stroke-primary-accent-mix: 24%;
|
|
--theme-stroke-secondary-accent-mix: 16%;
|
|
--theme-stroke-tertiary-accent-mix: 10%;
|
|
--theme-stroke-quaternary-accent-mix: 6%;
|
|
--theme-row-hover-accent-mix: 4%;
|
|
--theme-row-active-accent-mix: 8%;
|
|
--theme-control-hover-accent-mix: 6%;
|
|
--theme-control-active-accent-mix: 8%;
|
|
|
|
--ui-base: var(--theme-foreground);
|
|
--ui-accent: var(--theme-midground);
|
|
--ui-accent-secondary: var(--theme-primary);
|
|
--ui-warm: var(--theme-warm);
|
|
--ui-red: #cf2d56;
|
|
--ui-orange: #db704b;
|
|
--ui-yellow: #c08532;
|
|
--ui-green: #1f8a65;
|
|
--ui-cyan: #4c7f8c;
|
|
--ui-blue: #0053fd;
|
|
--ui-purple: #9e94d5;
|
|
--ui-bg-chrome: color-mix(
|
|
in srgb,
|
|
var(--theme-background-seed) var(--theme-mix-chrome),
|
|
var(--theme-neutral-chrome)
|
|
);
|
|
--ui-bg-sidebar: color-mix(
|
|
in srgb,
|
|
var(--theme-sidebar-seed) var(--theme-mix-sidebar),
|
|
var(--theme-neutral-sidebar)
|
|
);
|
|
--ui-bg-editor: color-mix(in srgb, var(--theme-card-seed) var(--theme-mix-card), var(--theme-neutral-card));
|
|
--ui-bg-elevated: color-mix(
|
|
in srgb,
|
|
var(--theme-elevated-seed) var(--theme-mix-elevated),
|
|
var(--theme-neutral-card)
|
|
);
|
|
--ui-bg-card: color-mix(in srgb, var(--ui-accent) 4%, color-mix(in srgb, var(--ui-base) 4%, transparent));
|
|
--ui-bg-input: #fcfcfc;
|
|
--ui-bg-primary: color-mix(
|
|
in srgb,
|
|
var(--ui-accent) var(--theme-fill-primary-accent-mix),
|
|
color-mix(in srgb, var(--ui-base) 10%, transparent)
|
|
);
|
|
--ui-bg-secondary: color-mix(
|
|
in srgb,
|
|
var(--ui-accent) var(--theme-fill-secondary-accent-mix),
|
|
color-mix(in srgb, var(--ui-base) 7%, transparent)
|
|
);
|
|
--ui-bg-tertiary: color-mix(
|
|
in srgb,
|
|
var(--ui-accent) var(--theme-fill-tertiary-accent-mix),
|
|
color-mix(in srgb, var(--ui-base) 5%, transparent)
|
|
);
|
|
--ui-bg-quaternary: color-mix(
|
|
in srgb,
|
|
var(--ui-accent) var(--theme-fill-quaternary-accent-mix),
|
|
color-mix(in srgb, var(--ui-base) 4%, transparent)
|
|
);
|
|
--ui-bg-quinary: color-mix(
|
|
in srgb,
|
|
var(--ui-accent) var(--theme-fill-quinary-accent-mix),
|
|
color-mix(in srgb, var(--ui-base) 3%, transparent)
|
|
);
|
|
--ui-row-hover-background: color-mix(
|
|
in srgb,
|
|
var(--ui-accent) var(--theme-row-hover-accent-mix),
|
|
color-mix(in srgb, var(--ui-base) 3%, transparent)
|
|
);
|
|
--ui-row-active-background: color-mix(
|
|
in srgb,
|
|
var(--ui-accent) var(--theme-row-active-accent-mix),
|
|
color-mix(in srgb, var(--ui-base) 5%, transparent)
|
|
);
|
|
--ui-control-hover-background: color-mix(
|
|
in srgb,
|
|
var(--ui-accent) var(--theme-control-hover-accent-mix),
|
|
color-mix(in srgb, var(--ui-base) 4%, transparent)
|
|
);
|
|
--ui-control-active-background: color-mix(
|
|
in srgb,
|
|
var(--ui-accent) var(--theme-control-active-accent-mix),
|
|
color-mix(in srgb, var(--ui-base) 5%, transparent)
|
|
);
|
|
--ui-text-primary: color-mix(in srgb, var(--ui-base) 94%, transparent);
|
|
--ui-text-secondary: color-mix(in srgb, var(--ui-base) 74%, transparent);
|
|
--ui-text-tertiary: color-mix(in srgb, var(--ui-base) 54%, transparent);
|
|
--ui-text-quaternary: color-mix(in srgb, var(--ui-base) 36%, transparent);
|
|
--ui-stroke-primary: color-mix(
|
|
in srgb,
|
|
var(--ui-accent) var(--theme-stroke-primary-accent-mix),
|
|
color-mix(in srgb, var(--ui-base) 10%, transparent)
|
|
);
|
|
--ui-stroke-secondary: color-mix(
|
|
in srgb,
|
|
var(--ui-accent) var(--theme-stroke-secondary-accent-mix),
|
|
color-mix(in srgb, var(--ui-base) 7%, transparent)
|
|
);
|
|
--ui-stroke-tertiary: color-mix(
|
|
in srgb,
|
|
var(--ui-accent) var(--theme-stroke-tertiary-accent-mix),
|
|
color-mix(in srgb, var(--ui-base) 5%, transparent)
|
|
);
|
|
--ui-stroke-quaternary: color-mix(
|
|
in srgb,
|
|
var(--ui-accent) var(--theme-stroke-quaternary-accent-mix),
|
|
color-mix(in srgb, var(--ui-base) 3%, transparent)
|
|
);
|
|
--ui-sash-hover-border: color-mix(in srgb, var(--ui-accent) 18%, var(--ui-stroke-tertiary));
|
|
--ui-sash-hover-background: color-mix(in srgb, var(--ui-accent) 6%, transparent);
|
|
--ui-surface-background: var(--ui-bg-editor);
|
|
--ui-sidebar-surface-background: var(--ui-bg-sidebar);
|
|
--ui-chat-surface-background: var(--ui-bg-chrome);
|
|
--ui-editor-surface-background: var(--ui-bg-chrome);
|
|
--ui-chat-bubble-background: color-mix(
|
|
in srgb,
|
|
var(--theme-bubble-seed) var(--theme-mix-bubble),
|
|
var(--theme-neutral-card)
|
|
);
|
|
--ui-chat-bubble-opaque-background: var(--ui-bg-editor);
|
|
--ui-inline-code-background: color-mix(in srgb, #141414 5%, transparent);
|
|
--ui-inline-code-border: color-mix(in srgb, #141414 8%, transparent);
|
|
--ui-inline-code-foreground: color-mix(in srgb, #141414 88%, transparent);
|
|
--ui-selection-background: color-mix(in srgb, #ffd24a 55%, transparent);
|
|
|
|
--dt-background: var(--ui-bg-chrome);
|
|
--dt-foreground: var(--ui-text-primary);
|
|
--dt-card: var(--ui-bg-editor);
|
|
--dt-card-foreground: var(--ui-text-primary);
|
|
--dt-muted: var(--ui-bg-tertiary);
|
|
--dt-muted-foreground: var(--ui-text-tertiary);
|
|
--dt-popover: color-mix(in srgb, var(--ui-bg-elevated) 96%, transparent);
|
|
--dt-popover-foreground: var(--ui-text-primary);
|
|
--dt-primary: var(--theme-primary);
|
|
--dt-primary-foreground: #fcfcfc;
|
|
--dt-secondary: var(--theme-secondary);
|
|
--dt-secondary-foreground: var(--ui-text-secondary);
|
|
--dt-accent: var(--theme-accent-soft);
|
|
--dt-accent-foreground: var(--ui-text-primary);
|
|
--dt-border: var(--ui-stroke-secondary);
|
|
--dt-input: var(--ui-stroke-primary);
|
|
--dt-ring: var(--ui-stroke-primary);
|
|
--dt-midground: var(--theme-midground);
|
|
--dt-composer-ring: var(--ui-base);
|
|
--dt-destructive: #cf2d56;
|
|
--dt-destructive-foreground: #ffffff;
|
|
--dt-sidebar-bg: var(--ui-bg-sidebar);
|
|
--dt-sidebar-border: var(--ui-stroke-secondary);
|
|
--dt-user-bubble: var(--ui-chat-bubble-background);
|
|
--dt-user-bubble-border: var(--ui-stroke-tertiary);
|
|
|
|
--dt-font-sans: 'Segoe WPC', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif,
|
|
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji', emoji;
|
|
--dt-font-mono: 'Cascadia Code', 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, Consolas, monospace,
|
|
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji', emoji;
|
|
--dt-base-size: 1rem;
|
|
--dt-line-height: 1.5;
|
|
--dt-letter-spacing: 0;
|
|
--dt-spacing-mul: 1;
|
|
|
|
--radius: 0.75rem;
|
|
--radius-scalar: 0.6;
|
|
|
|
/* Space under last message vs overlay composer — driven by the measured composer height (see composer/index.tsx). */
|
|
--thread-last-message-clearance: calc(var(--composer-measured-height) + 2rem);
|
|
|
|
--composer-shell-pad-block-end: 0.625rem;
|
|
--message-text-indent: 0.75rem;
|
|
--conversation-text-font-size: 0.8125rem;
|
|
--conversation-tool-font-size: 0.6875rem;
|
|
--conversation-caption-font-size: 0.75rem;
|
|
--conversation-line-height: 1.125rem;
|
|
--conversation-caption-line-height: 1rem;
|
|
--conversation-turn-gap: 0.375rem;
|
|
/* Gap between top-level turn blocks (prose ↔ tools ↔ thinking) — enough air
|
|
that scaffolding reads as separate from the reply, not crammed into it. */
|
|
--turn-block-gap: 0.75rem;
|
|
/* Tight gap between tool rows inside a single action group, so a back-to-back
|
|
run still reads as one cohesive sequence. */
|
|
--tool-row-gap: 0.375rem;
|
|
/* Paragraph spacing — vertical gap between prose paragraphs, both inside a
|
|
markdown block and between consecutive prose parts. Single knob; tweak
|
|
freely. */
|
|
--paragraph-gap: 0.45rem;
|
|
--sticky-human-top: 0.23rem;
|
|
--file-tree-row-height: 1.375rem;
|
|
|
|
--composer-width: 48.75rem;
|
|
--composer-control-size: 1.75rem;
|
|
--composer-control-primary-size: 1.875rem;
|
|
--composer-control-gap: 0.25rem;
|
|
--composer-row-gap: 0.25rem;
|
|
--composer-ring-strength: 1;
|
|
--composer-surface-pad-x: 0.5rem;
|
|
--composer-surface-pad-y: 0.3125rem;
|
|
--composer-input-min-height: 1.625rem;
|
|
--composer-input-max-height: 9.375rem;
|
|
--composer-input-inline-min-width: 8rem;
|
|
--composer-fallback-height: 2.75rem;
|
|
--composer-measured-height: calc(0.5rem + var(--composer-shell-pad-block-end) + var(--composer-fallback-height));
|
|
--composer-surface-measured-height: var(--composer-fallback-height);
|
|
--thread-viewport-height: max(
|
|
0rem,
|
|
calc(100% - var(--composer-measured-height) + var(--composer-surface-measured-height))
|
|
);
|
|
--vsq: min(0.5vh, 0.5vw);
|
|
--image-preview-max-width: 34rem;
|
|
--image-preview-height: clamp(16.25rem, calc(var(--vsq) * 100), 26.25rem);
|
|
|
|
--sidebar-width: 14.8125rem;
|
|
--chat-min-width: 28rem;
|
|
--titlebar-control-size: 1.25rem;
|
|
--titlebar-control-height: 1.375rem;
|
|
--sidebar-content-inline-padding: 1rem;
|
|
|
|
--sidebar: var(--dt-sidebar-bg);
|
|
--sidebar-foreground: var(--dt-foreground);
|
|
--sidebar-primary: var(--dt-primary);
|
|
--sidebar-primary-foreground: var(--dt-primary-foreground);
|
|
--sidebar-accent: var(--ui-control-active-background);
|
|
--sidebar-accent-foreground: var(--dt-accent-foreground);
|
|
--sidebar-border: var(--dt-sidebar-border);
|
|
--sidebar-ring: var(--dt-ring);
|
|
--sidebar-edge-border: color-mix(in srgb, var(--ui-base) 7.5%, transparent);
|
|
--chrome-action-hover: var(--ui-control-hover-background);
|
|
|
|
--midground: var(--dt-midground);
|
|
--background: var(--dt-background);
|
|
--foreground: var(--dt-foreground);
|
|
|
|
--warm-glow: color-mix(in srgb, var(--ui-warm) 32%, color-mix(in srgb, var(--ui-accent) 6%, transparent));
|
|
/* `--noise-opacity-mul` is set per-mode by `applyTheme()`. */
|
|
--noise-opacity-mul: 1;
|
|
--backdrop-invert-mul: 1;
|
|
}
|
|
|
|
:root.dark {
|
|
/* Per-mode mix knobs — overridden inline by `applyTheme()` per skin. */
|
|
--theme-mix-chrome: 74%;
|
|
--theme-mix-card: 38%;
|
|
--theme-mix-elevated: 46%;
|
|
--theme-mix-bubble: 46%;
|
|
--theme-neutral-chrome: #0d0d0e;
|
|
--theme-neutral-sidebar: #0a0a0b;
|
|
--theme-neutral-card: #161618;
|
|
|
|
/* Dark-only accent palette overrides. */
|
|
--ui-red: #e75e78;
|
|
--ui-green: #55a583;
|
|
--ui-cyan: #6f9ba6;
|
|
|
|
--sidebar-edge-border: color-mix(in srgb, var(--ui-base) 12%, transparent);
|
|
--composer-ring-strength: 1.3;
|
|
--backdrop-invert-mul: 0;
|
|
|
|
--ui-inline-code-background: color-mix(in srgb, #ffffff 7%, transparent);
|
|
--ui-inline-code-border: color-mix(in srgb, #ffffff 10%, transparent);
|
|
--ui-inline-code-foreground: color-mix(in srgb, #ffffff 88%, transparent);
|
|
--ui-selection-background: color-mix(in srgb, #ffd24a 38%, transparent);
|
|
}
|
|
|
|
* {
|
|
box-sizing: border-box;
|
|
border-color: var(--dt-border);
|
|
}
|
|
|
|
html,
|
|
body,
|
|
#root {
|
|
height: 100%;
|
|
}
|
|
|
|
html {
|
|
font-size: var(--dt-base-size, 0.875rem);
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
background: var(--ui-chat-surface-background);
|
|
color: var(--dt-foreground);
|
|
font-family: var(--dt-font-sans);
|
|
font-size: 0.8125rem;
|
|
line-height: var(--dt-line-height, 1.55);
|
|
letter-spacing: var(--dt-letter-spacing, 0);
|
|
overflow: hidden;
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
-webkit-font-smoothing: antialiased;
|
|
}
|
|
|
|
button,
|
|
textarea {
|
|
font: inherit;
|
|
}
|
|
|
|
:where(
|
|
a,
|
|
.underline,
|
|
[class~='hover:underline'],
|
|
[class~='focus:underline'],
|
|
[class~='focus-visible:underline'],
|
|
[class~='group-hover:underline'],
|
|
[class~='peer-hover:underline']
|
|
) {
|
|
text-decoration-color: color-mix(in srgb, currentColor 20%, transparent);
|
|
text-underline-offset: 0.25rem;
|
|
}
|
|
|
|
*::selection {
|
|
background: var(--ui-selection-background);
|
|
color: inherit;
|
|
}
|
|
}
|
|
|
|
.dither {
|
|
background: repeating-conic-gradient(currentColor 0% 25%, transparent 0% 50%) 0 0 / 0.125rem 0.125rem;
|
|
}
|
|
|
|
:root:not([style*='--theme-asset-bg:']) .theme-default-filler {
|
|
display: block;
|
|
}
|
|
|
|
:root[style*='--theme-asset-bg:'] .theme-default-filler {
|
|
display: none;
|
|
}
|
|
|
|
/* Primitive-level pointer cursor for every interactive control (buttons,
|
|
selects, menu items, switches, tabs, summaries). Keeps individual
|
|
components from having to hardcode `cursor-pointer`; explicit cursor
|
|
utilities (cursor-grab, cursor-default, disabled:cursor-*) still win since
|
|
they live in the utilities layer. */
|
|
@layer base {
|
|
button:not(:disabled):not([aria-disabled='true']),
|
|
summary,
|
|
[role='button']:not([aria-disabled='true']),
|
|
[role='menuitem']:not([aria-disabled='true']),
|
|
[role='menuitemradio']:not([aria-disabled='true']),
|
|
[role='menuitemcheckbox']:not([aria-disabled='true']),
|
|
[role='option']:not([aria-disabled='true']),
|
|
[role='switch']:not([aria-disabled='true']),
|
|
[role='tab']:not([aria-disabled='true']) {
|
|
cursor: pointer;
|
|
}
|
|
}
|
|
|
|
@layer utilities {
|
|
[class*='rounded-full'],
|
|
[class*=':rounded-full'] {
|
|
border-radius: calc(var(--radius-scalar) * 9999rem);
|
|
}
|
|
}
|
|
|
|
@keyframes arc-border {
|
|
0% {
|
|
background-position: 15% 15%;
|
|
}
|
|
100% {
|
|
background-position: 75% 75%;
|
|
}
|
|
}
|
|
|
|
.arc-border {
|
|
--arc-c0: color-mix(in srgb, var(--dt-foreground) 0%, transparent);
|
|
--arc-c1: var(--dt-midground);
|
|
--arc-c2: var(--dt-background);
|
|
--arc-angle: 160deg;
|
|
--arc-width: 0.078125rem;
|
|
--arc-inset: -0.125rem;
|
|
--arc-duration: 2.23s;
|
|
|
|
pointer-events: none;
|
|
position: absolute;
|
|
overflow: hidden;
|
|
border-radius: inherit;
|
|
inset: var(--arc-inset);
|
|
padding: var(--arc-width);
|
|
mask:
|
|
linear-gradient(#000 0 0) content-box,
|
|
linear-gradient(#000 0 0);
|
|
-webkit-mask-composite: xor;
|
|
mask-composite: exclude;
|
|
}
|
|
|
|
:root.dark .arc-border {
|
|
--arc-c1: var(--dt-foreground);
|
|
}
|
|
|
|
/* Quest-style "needs you" pulse for a clarify-blocked session's dot —
|
|
a soft amber glow that breathes so the row draws the eye without a toast. */
|
|
@keyframes quest-glow {
|
|
0%,
|
|
100% {
|
|
transform: scale(1);
|
|
box-shadow:
|
|
0 0 0.1875rem color-mix(in srgb, var(--color-amber-500, #f59e0b) 70%, transparent),
|
|
0 0 0.5rem color-mix(in srgb, var(--color-amber-500, #f59e0b) 45%, transparent);
|
|
}
|
|
50% {
|
|
transform: scale(1.18);
|
|
box-shadow:
|
|
0 0 0.3125rem color-mix(in srgb, var(--color-amber-500, #f59e0b) 90%, transparent),
|
|
0 0 0.875rem color-mix(in srgb, var(--color-amber-500, #f59e0b) 65%, transparent);
|
|
}
|
|
}
|
|
|
|
.quest-glow {
|
|
animation: quest-glow 1.8s ease-in-out infinite;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.quest-glow {
|
|
animation: none;
|
|
box-shadow:
|
|
0 0 0.25rem color-mix(in srgb, var(--color-amber-500, #f59e0b) 80%, transparent),
|
|
0 0 0.625rem color-mix(in srgb, var(--color-amber-500, #f59e0b) 55%, transparent);
|
|
}
|
|
}
|
|
|
|
/* Command-palette deep-link: briefly flash the targeted settings row. */
|
|
@keyframes setting-field-flash {
|
|
0% {
|
|
background-color: color-mix(in srgb, var(--dt-primary, #f59e0b) 22%, transparent);
|
|
}
|
|
100% {
|
|
background-color: transparent;
|
|
}
|
|
}
|
|
|
|
.setting-field-highlight {
|
|
animation: setting-field-flash 1.6s ease-out;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.setting-field-highlight {
|
|
animation: none;
|
|
}
|
|
}
|
|
|
|
.arc-border::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: inherit;
|
|
background: linear-gradient(
|
|
var(--arc-angle),
|
|
transparent 0%,
|
|
var(--arc-c0) 15%,
|
|
var(--arc-c1) 20%,
|
|
var(--arc-c2) 25%,
|
|
transparent 35%,
|
|
transparent 40%,
|
|
var(--arc-c0) 55%,
|
|
var(--arc-c1) 60%,
|
|
var(--arc-c2) 65%,
|
|
transparent 75%,
|
|
transparent 80%,
|
|
var(--arc-c0) 95%,
|
|
var(--arc-c1) 100%
|
|
);
|
|
background-size: 300% 300%;
|
|
animation: arc-border var(--arc-duration) linear infinite;
|
|
}
|
|
|
|
/* Flip the arc's travel direction (e.g. the Nous Portal hero row). */
|
|
.arc-border.arc-reverse::before {
|
|
animation-direction: reverse;
|
|
}
|
|
|
|
/* Nous Portal hero: slower, blue → orange arc. */
|
|
.arc-border.arc-nous,
|
|
:root.dark .arc-border.arc-nous {
|
|
--arc-c1: #4f8cff;
|
|
--arc-c2: #ff8c42;
|
|
--arc-duration: 3.27s;
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.arc-border::before {
|
|
animation: none;
|
|
}
|
|
}
|
|
|
|
/* No focus rings, anywhere. Kills the native outline plus Tailwind's
|
|
`focus-visible:ring-*` (a box-shadow driven by --tw-ring-*). Unlayered so it
|
|
beats the utilities layer without !important on the outline. The composer /
|
|
.desktop-input-chrome focus glow is untouched — those set `box-shadow`
|
|
directly rather than through the ring vars. */
|
|
*:focus,
|
|
*:focus-visible {
|
|
outline: none;
|
|
}
|
|
|
|
*:focus-visible {
|
|
--tw-ring-shadow: 0 0 #0000 !important;
|
|
--tw-ring-offset-shadow: 0 0 #0000 !important;
|
|
}
|
|
|
|
button {
|
|
-webkit-app-region: no-drag;
|
|
}
|
|
|
|
/* Button variant styling lives entirely in the cva in components/ui/button.tsx
|
|
(the single source of truth). Don't re-add [data-slot='button'] rules here —
|
|
attribute selectors out-specify the Tailwind utilities and silently override
|
|
the variants. */
|
|
|
|
[data-slot='dropdown-menu-content'],
|
|
[data-slot='select-content'],
|
|
[data-slot='dialog-content'] {
|
|
border-color: var(--ui-stroke-secondary);
|
|
background: color-mix(in srgb, var(--ui-bg-elevated) 96%, transparent);
|
|
box-shadow: var(--shadow-md);
|
|
backdrop-filter: blur(0.75rem) saturate(1.08);
|
|
-webkit-backdrop-filter: blur(0.75rem) saturate(1.08);
|
|
}
|
|
|
|
[data-slot='dropdown-menu-item']:focus,
|
|
[data-slot='dropdown-menu-checkbox-item']:focus,
|
|
[data-slot='dropdown-menu-radio-item']:focus {
|
|
background: var(--ui-bg-tertiary);
|
|
color: var(--ui-text-primary);
|
|
}
|
|
|
|
input,
|
|
textarea,
|
|
[contenteditable]:not([contenteditable='false']),
|
|
[data-slot='aui_user-message-root'],
|
|
[data-slot='aui_assistant-message-content'],
|
|
[data-selectable-text='true'],
|
|
[data-selectable-text='true'] * {
|
|
-webkit-user-select: text;
|
|
user-select: text;
|
|
}
|
|
|
|
button,
|
|
[role='button'] {
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
}
|
|
|
|
img,
|
|
picture,
|
|
video,
|
|
canvas,
|
|
svg {
|
|
-webkit-user-select: none;
|
|
user-select: none;
|
|
}
|
|
|
|
img,
|
|
video,
|
|
canvas {
|
|
-webkit-user-drag: none;
|
|
}
|
|
|
|
/* Shared input chrome — mirrors composer hover/focus FX. Unlayered to beat Tailwind utilities. */
|
|
.desktop-input-chrome {
|
|
--ring-pct: 18%;
|
|
--ring-fall: var(--dt-input);
|
|
background: color-mix(in srgb, var(--dt-card) 68%, transparent);
|
|
border-color: color-mix(
|
|
in srgb,
|
|
var(--dt-composer-ring) calc(var(--ring-pct) * var(--composer-ring-strength)),
|
|
var(--ring-fall)
|
|
);
|
|
box-shadow: none;
|
|
transition:
|
|
background-color 200ms ease-out,
|
|
border-color 200ms ease-out;
|
|
}
|
|
|
|
.desktop-input-chrome:hover {
|
|
--ring-pct: 30%;
|
|
background: color-mix(in srgb, var(--dt-card) 86%, transparent);
|
|
}
|
|
|
|
.desktop-input-chrome:focus {
|
|
--ring-pct: 45%;
|
|
--ring-fall: transparent;
|
|
background: var(--dt-card);
|
|
box-shadow: none;
|
|
outline: none;
|
|
}
|
|
|
|
.desktop-input-chrome[aria-invalid='true'] {
|
|
border-color: var(--dt-destructive);
|
|
}
|
|
|
|
@layer components {
|
|
.scrollbar-dt,
|
|
.scrollbar-dt * {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: color-mix(in srgb, var(--dt-midground) 18%, transparent) transparent;
|
|
}
|
|
|
|
.scrollbar-dt::-webkit-scrollbar,
|
|
.scrollbar-dt *::-webkit-scrollbar {
|
|
width: 0.5rem;
|
|
height: 0.5rem;
|
|
}
|
|
|
|
.scrollbar-dt::-webkit-scrollbar-track,
|
|
.scrollbar-dt::-webkit-scrollbar-corner,
|
|
.scrollbar-dt *::-webkit-scrollbar-track,
|
|
.scrollbar-dt *::-webkit-scrollbar-corner {
|
|
background: transparent;
|
|
}
|
|
|
|
.scrollbar-dt::-webkit-scrollbar-thumb,
|
|
.scrollbar-dt *::-webkit-scrollbar-thumb {
|
|
background: color-mix(in srgb, var(--dt-midground) 18%, transparent);
|
|
border-radius: 9999rem;
|
|
border: 0.125rem solid transparent;
|
|
background-clip: padding-box;
|
|
}
|
|
|
|
.scrollbar-dt::-webkit-scrollbar-thumb:hover,
|
|
.scrollbar-dt *::-webkit-scrollbar-thumb:hover {
|
|
background: color-mix(in srgb, var(--dt-midground) 40%, transparent);
|
|
background-clip: padding-box;
|
|
}
|
|
|
|
.scrollbar-dt::-webkit-scrollbar-button,
|
|
.scrollbar-dt *::-webkit-scrollbar-button {
|
|
display: none;
|
|
}
|
|
|
|
/* Variant for portaled overlays (Radix DropdownMenu, Popover, etc.) that
|
|
render under document.body, outside the `.scrollbar-dt` scope on
|
|
#root. Same visual treatment, applied directly to the overlay
|
|
container so its (and only its) internal scrollbar is themed. */
|
|
.dt-portal-scrollbar {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: color-mix(in srgb, var(--dt-midground) 28%, transparent) transparent;
|
|
}
|
|
|
|
.dt-portal-scrollbar::-webkit-scrollbar {
|
|
width: 0.375rem;
|
|
height: 0.375rem;
|
|
}
|
|
|
|
.dt-portal-scrollbar::-webkit-scrollbar-track,
|
|
.dt-portal-scrollbar::-webkit-scrollbar-corner {
|
|
background: transparent;
|
|
}
|
|
|
|
.dt-portal-scrollbar::-webkit-scrollbar-thumb {
|
|
background: color-mix(in srgb, var(--dt-midground) 28%, transparent);
|
|
border-radius: 9999rem;
|
|
border: 0.0625rem solid transparent;
|
|
background-clip: padding-box;
|
|
}
|
|
|
|
.dt-portal-scrollbar::-webkit-scrollbar-thumb:hover {
|
|
background: color-mix(in srgb, var(--dt-midground) 50%, transparent);
|
|
background-clip: padding-box;
|
|
}
|
|
|
|
.dt-portal-scrollbar::-webkit-scrollbar-button {
|
|
display: none;
|
|
}
|
|
}
|
|
|
|
/* Bottom clearance lives on [data-slot='aui_composer-clearance'] —
|
|
virtualized items unmount, so :nth-last-child can't fire reliably. */
|
|
|
|
[data-slot='aui_assistant-message-content'] {
|
|
padding-left: var(--message-text-indent);
|
|
font-size: var(--conversation-text-font-size);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
[data-slot='aui_assistant-message-root'] {
|
|
width: 100%;
|
|
}
|
|
|
|
[data-slot='aui_assistant-message-content'] .aui-md,
|
|
[data-slot='aui_assistant-message-content'] .aui-md :where(p, li, blockquote, table, pre) {
|
|
font-size: inherit;
|
|
}
|
|
|
|
/* Tailwind Typography sets `.prose :where(p) { margin: 1.25em }` (~16px). That
|
|
selector ties our `my-*` utility on specificity and wins on source order, so
|
|
paragraph spacing must be reclaimed here at higher specificity. One tight
|
|
top-margin (bottom zeroed to avoid doubling), first child reset to flush. */
|
|
[data-slot='aui_assistant-message-content'] .aui-md :where(p) {
|
|
margin-block: var(--paragraph-gap) 0;
|
|
}
|
|
|
|
/* First rendered element of a prose block is flush — the block-level gap above
|
|
(tool / paragraph) already provides the separation. Reach one level deep too:
|
|
Streamdown wraps blocks in a `div.space-y-*`, so the real first line is the
|
|
first child's first child. */
|
|
[data-slot='aui_assistant-message-content'] .aui-md > :first-child,
|
|
[data-slot='aui_assistant-message-content'] .aui-md > :first-child > :first-child {
|
|
margin-top: 0;
|
|
}
|
|
|
|
/* Prose, tools, todos, and thinking all share one left edge (the message
|
|
content's --message-text-indent). No extra prose indent — a single gutter
|
|
reads cleaner than a ragged tool-vs-reply column. */
|
|
|
|
[data-slot='aui_user-message-root'] {
|
|
top: var(--sticky-human-top);
|
|
}
|
|
|
|
[data-slot='aui_user-message-root'],
|
|
[data-slot='aui_edit-composer-root'] {
|
|
font-size: var(--conversation-text-font-size);
|
|
}
|
|
|
|
/* Sticky human bubbles clamp to ~2 lines with a soft bottom fade so a long
|
|
prompt doesn't dominate the viewport. The clamp lifts on focus only (clicking
|
|
opens the edit composer, which shows the full text) — not on hover, so the
|
|
bubble doesn't jump as the pointer passes over it. --human-msg-full is the
|
|
measured content height (set in UserMessage) so it animates to the real
|
|
height instead of overshooting the cap. */
|
|
.sticky-human-clamp {
|
|
cursor: pointer;
|
|
max-height: calc(2 * var(--dt-line-height) * var(--conversation-text-font-size) + 0.15rem);
|
|
overflow: hidden;
|
|
transition: max-height 0.08s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.sticky-human-clamp[data-clamped='true'] {
|
|
-webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent);
|
|
mask-image: linear-gradient(to bottom, #000 55%, transparent);
|
|
}
|
|
|
|
.composer-human-message:focus-within .sticky-human-clamp {
|
|
max-height: min(var(--human-msg-full, 24rem), 24rem);
|
|
overflow-y: auto;
|
|
-webkit-mask-image: none;
|
|
mask-image: none;
|
|
}
|
|
|
|
/* The thread renders items in natural document flow (padding spacers, not
|
|
transforms) and @tanstack/react-virtual already adjusts scrollTop itself
|
|
when an off-screen turn is measured and its real height differs from the
|
|
220px estimate. The browser's native scroll anchoring (overflow-anchor:
|
|
auto) would adjust scrollTop for that SAME size delta, so the two
|
|
double-correct and the view lurches — most visibly on Windows mouse wheels,
|
|
whose coarse notches mount/measure several under-estimated turns per tick.
|
|
Opt out of native anchoring so only the virtualizer compensates. */
|
|
[data-slot='aui_thread-viewport'] {
|
|
overflow-anchor: none;
|
|
}
|
|
|
|
[data-slot='aui_thread-content'] {
|
|
max-width: var(--composer-width);
|
|
padding-inline: 1.5rem;
|
|
}
|
|
|
|
[data-slot='aui_intro'] {
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding-bottom: var(--composer-measured-height);
|
|
text-align: center;
|
|
}
|
|
|
|
[data-slot='aui_intro'] > div {
|
|
max-width: min(var(--composer-width), 82vw);
|
|
}
|
|
|
|
[data-slot='aui_intro'] p:last-child {
|
|
max-width: 34rem;
|
|
margin-inline: auto;
|
|
color: var(--ui-text-tertiary);
|
|
font-size: 0.875rem;
|
|
line-height: 1.45;
|
|
}
|
|
|
|
.fit-text {
|
|
--fit-captured-length: initial;
|
|
--fit-support-sentinel: var(--fit-captured-length, 9999px);
|
|
|
|
display: flex;
|
|
container-type: inline-size;
|
|
}
|
|
|
|
.fit-text > [aria-hidden] {
|
|
visibility: hidden;
|
|
}
|
|
|
|
.fit-text > :not([aria-hidden]) {
|
|
flex-grow: 1;
|
|
container-type: inline-size;
|
|
|
|
--fit-captured-length: 100cqi;
|
|
--fit-available-space: var(--fit-captured-length);
|
|
}
|
|
|
|
.fit-text > :not([aria-hidden]) > * {
|
|
--fit-support-sentinel: inherit;
|
|
--fit-captured-length: 100cqi;
|
|
--fit-ratio: tan(atan2(var(--fit-available-space), var(--fit-available-space) - var(--fit-captured-length)));
|
|
|
|
display: block;
|
|
inline-size: var(--fit-available-space);
|
|
font-size: clamp(var(--fit-min, 1em), 1em * var(--fit-ratio), var(--fit-max, infinity * 1px) - var(--fit-support-sentinel));
|
|
}
|
|
|
|
@container (inline-size > 0) {
|
|
.fit-text > :not([aria-hidden]) > * {
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
|
|
@property --fit-captured-length {
|
|
syntax: '<length>';
|
|
initial-value: 0px;
|
|
inherits: true;
|
|
}
|
|
|
|
[data-slot='composer-root'] {
|
|
width: min(var(--composer-width), calc(100% - 2rem));
|
|
padding-bottom: var(--composer-shell-pad-block-end);
|
|
}
|
|
|
|
[data-slot='composer-root'] > .pointer-events-none {
|
|
background: linear-gradient(
|
|
to bottom,
|
|
transparent,
|
|
color-mix(in srgb, var(--ui-chat-surface-background) 88%, transparent)
|
|
) !important;
|
|
}
|
|
|
|
[data-slot='composer-surface'] {
|
|
border-color: var(--ui-stroke-secondary) !important;
|
|
}
|
|
|
|
[data-slot='composer-fade'] {
|
|
min-height: 2.375rem;
|
|
}
|
|
|
|
[data-slot='composer-rich-input'] {
|
|
color: var(--ui-text-primary);
|
|
font-size: 0.8125rem;
|
|
}
|
|
|
|
[data-slot='composer-rich-input']:empty::before {
|
|
color: var(--ui-text-tertiary) !important;
|
|
}
|
|
|
|
[data-slot='composer-root']:focus-within [data-slot='composer-surface'] > [aria-hidden='true'] {
|
|
background: var(--ui-chat-bubble-background) !important;
|
|
}
|
|
|
|
/* Tool/thinking blocks now live at message-text alignment (no leading
|
|
chevron column to escape into), so their headers and bodies share a
|
|
common left edge with the model's text. */
|
|
[data-slot='aui_assistant-message-content'] > [data-slot='tool-block'],
|
|
[data-slot='aui_assistant-message-content'] > [data-slot='aui_thinking-disclosure'] {
|
|
width: 100%;
|
|
max-width: 100%;
|
|
}
|
|
|
|
[data-slot='aui_assistant-message-content'] .aui-md [data-streamdown='code-block'] code {
|
|
max-width: none;
|
|
font-family: inherit;
|
|
font-size: inherit;
|
|
padding: 0;
|
|
border-radius: 0;
|
|
background: transparent;
|
|
color: inherit;
|
|
overflow-x: visible;
|
|
overflow-wrap: inherit;
|
|
vertical-align: baseline;
|
|
word-break: inherit;
|
|
white-space: inherit;
|
|
}
|
|
|
|
/* Streamdown's adapter wraps code fences in a `data-streamdown="code-block"`
|
|
container with its own card chrome. We render our own <CodeCard>, so this
|
|
strips the upstream chrome down to a layout-only passthrough. */
|
|
[data-slot='aui_assistant-message-content'] .aui-md [data-streamdown='code-block'] {
|
|
contain: none;
|
|
overflow: visible;
|
|
margin-block: var(--paragraph-gap) 0 !important;
|
|
padding: 0 !important;
|
|
gap: 0 !important;
|
|
border: 0 !important;
|
|
border-radius: 0 !important;
|
|
background: transparent !important;
|
|
color: inherit;
|
|
}
|
|
|
|
[data-slot='aui_assistant-message-content'] .aui-md [data-streamdown='code-block']:has(.aui-prose-fence) {
|
|
margin-block: 0 !important;
|
|
}
|
|
|
|
[data-slot='aui_assistant-message-content'] .aui-md [data-slot='code-card'] {
|
|
/* Streamdown nests blocks, so the container's child-combinator rhythm can't
|
|
reach the card. Carry the paragraph gap on the card itself (top-owned);
|
|
collapses cleanly with the wrapper's margin when one is present, and the
|
|
first-child reset still flushes a leading code block. */
|
|
margin-block: var(--paragraph-gap) 0;
|
|
position: relative;
|
|
transition:
|
|
border-color 180ms ease-out,
|
|
box-shadow 180ms ease-out,
|
|
background-color 180ms ease-out;
|
|
}
|
|
|
|
[data-slot='aui_assistant-message-content'] .aui-md [data-slot='code-card'][data-streaming='true'] {
|
|
animation:
|
|
code-card-stream-enter 180ms cubic-bezier(0.16, 1, 0.3, 1) both,
|
|
code-card-stream-glow 1.8s ease-in-out 180ms infinite alternate;
|
|
border-color: color-mix(in srgb, var(--dt-ring) 24%, var(--ui-stroke-tertiary));
|
|
box-shadow:
|
|
0 0 0 0.0625rem color-mix(in srgb, var(--dt-ring) 10%, transparent),
|
|
0 0.625rem 1.75rem color-mix(in srgb, var(--dt-ring) 8%, transparent);
|
|
}
|
|
|
|
[data-slot='aui_assistant-message-content']
|
|
.aui-md
|
|
[data-slot='code-card'][data-streaming='true']
|
|
[data-slot='code-card-body'] {
|
|
-webkit-mask-image: linear-gradient(to bottom, black 0%, black calc(100% - 1.5rem), rgb(0 0 0 / 64%) 100%);
|
|
mask-image: linear-gradient(to bottom, black 0%, black calc(100% - 1.5rem), rgb(0 0 0 / 64%) 100%);
|
|
}
|
|
|
|
[data-slot='aui_assistant-message-content'] .aui-md :not(pre) > code {
|
|
border: 0.0625rem solid var(--ui-inline-code-border);
|
|
background: var(--ui-inline-code-background);
|
|
color: var(--ui-inline-code-foreground);
|
|
}
|
|
|
|
[data-slot='aui_assistant-message-content'] .aui-md :where(.aui-shiki, .aui-shiki > pre) {
|
|
margin: 0 !important;
|
|
}
|
|
|
|
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table {
|
|
border-spacing: 0;
|
|
}
|
|
|
|
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table > table,
|
|
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table thead,
|
|
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table tbody,
|
|
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table tr,
|
|
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table th,
|
|
[data-slot='aui_assistant-message-content'] .aui-md .aui-md-table td {
|
|
margin: 0 !important;
|
|
margin-block-start: 0 !important;
|
|
margin-block-end: 0 !important;
|
|
}
|
|
|
|
/* Tool / thinking blocks are scaffolding around the model's reply, so we
|
|
keep them transparent and fade them slightly. The reading column (prose)
|
|
stays at full strength; scaffolding lifts back to full opacity on
|
|
hover/focus so it stays legible when the user actually wants to read it. */
|
|
[data-slot='tool-block'],
|
|
[data-slot='aui_thinking-disclosure'] {
|
|
background: transparent !important;
|
|
}
|
|
|
|
[data-slot='aui_assistant-message-content'] > :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure']) {
|
|
opacity: 0.67;
|
|
transition: opacity 120ms ease-out;
|
|
}
|
|
|
|
[data-slot='aui_assistant-message-content']
|
|
> :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure']):is(:hover, :focus-within) {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Conversation block rhythm. assistant-ui renders each range as a direct child
|
|
of the message content with no per-part wrapper, so adjacency rules cover
|
|
every pairing — first block needs no reset, nested tool rows are untouched.
|
|
Two tiers: scaffolding (tool / thinking) gets a roomy block gap so it reads
|
|
as separate from the reply; consecutive prose collapses to a tight paragraph
|
|
rhythm so split-out text parts don't look like a big gap. */
|
|
/* Scaffolding adjacent to anything → roomy block gap. */
|
|
[data-slot='aui_assistant-message-content']
|
|
> :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure'])
|
|
+ :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure'], .aui-md),
|
|
[data-slot='aui_assistant-message-content']
|
|
> .aui-md
|
|
+ :is([data-slot='tool-block'], [data-slot='aui_thinking-disclosure']) {
|
|
margin-top: var(--turn-block-gap);
|
|
}
|
|
|
|
/* Prose ↔ prose → tight paragraph rhythm, matching in-block paragraph spacing. */
|
|
[data-slot='aui_assistant-message-content'] > .aui-md + .aui-md {
|
|
margin-top: var(--paragraph-gap);
|
|
}
|
|
|
|
/* Message action bars — flat icon hits with default dim; only the hovered/focused control is full-strength. */
|
|
[data-slot='aui_msg-actions'] button {
|
|
border: 0;
|
|
border-radius: 0;
|
|
background: transparent;
|
|
box-shadow: none;
|
|
padding: 0;
|
|
gap: 0;
|
|
height: auto;
|
|
width: auto;
|
|
min-height: 0;
|
|
min-width: 0;
|
|
flex-shrink: 0;
|
|
cursor: pointer;
|
|
color: var(--color-muted-foreground);
|
|
opacity: 0.5;
|
|
}
|
|
|
|
[data-slot='aui_msg-actions'] button:disabled {
|
|
cursor: default;
|
|
}
|
|
|
|
[data-slot='aui_msg-actions'] button:hover {
|
|
background: transparent;
|
|
color: var(--color-foreground);
|
|
opacity: 1;
|
|
}
|
|
|
|
[data-slot='aui_msg-actions'] button:active {
|
|
background: transparent;
|
|
}
|
|
|
|
[data-slot='aui_msg-actions'] button:focus-visible {
|
|
opacity: 1;
|
|
}
|
|
|
|
[data-slot='aui_msg-actions'] button svg {
|
|
width: 0.875rem;
|
|
height: 0.875rem;
|
|
}
|
|
|
|
/* Live thinking preview window. Pairs with the ResizeObserver in
|
|
ThinkingDisclosure that pins scrollTop to the bottom — older lines fade
|
|
into the top mask while the latest tokens settle in below. */
|
|
.thinking-preview {
|
|
-webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 28%, black 100%);
|
|
mask-image: linear-gradient(to bottom, transparent 0%, black 28%, black 100%);
|
|
}
|
|
|
|
@keyframes code-card-stream-enter {
|
|
from {
|
|
opacity: 0.74;
|
|
transform: translateY(0.375rem);
|
|
}
|
|
|
|
to {
|
|
opacity: 1;
|
|
transform: translateY(0);
|
|
}
|
|
}
|
|
|
|
@keyframes code-card-stream-glow {
|
|
from {
|
|
border-color: color-mix(in srgb, var(--dt-ring) 18%, var(--ui-stroke-tertiary));
|
|
box-shadow:
|
|
0 0 0 0.0625rem color-mix(in srgb, var(--dt-ring) 6%, transparent),
|
|
0 0.5rem 1.5rem color-mix(in srgb, var(--dt-ring) 5%, transparent);
|
|
}
|
|
|
|
to {
|
|
border-color: color-mix(in srgb, var(--dt-ring) 32%, var(--ui-stroke-tertiary));
|
|
box-shadow:
|
|
0 0 0 0.0625rem color-mix(in srgb, var(--dt-ring) 12%, transparent),
|
|
0 0.75rem 2rem color-mix(in srgb, var(--dt-ring) 10%, transparent);
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
[data-slot='aui_assistant-message-content'] .aui-md [data-slot='code-card'][data-streaming='true'] {
|
|
animation: none;
|
|
}
|
|
}
|
|
|
|
/* ── Keybind panel / edit overlay: small key chips ──────────────────────────
|
|
A quiet `kbd`-style chip shared by the shortcuts panel and the on-screen
|
|
editor so both read as the same control. No animation, no glow. */
|
|
.kbd-cap {
|
|
display: inline-grid;
|
|
place-items: center;
|
|
min-width: 1.5rem;
|
|
height: 1.4rem;
|
|
padding: 0 0.4rem;
|
|
border-radius: 0.375rem;
|
|
font-family: var(--dt-font-mono, ui-monospace, monospace);
|
|
font-size: 0.72rem;
|
|
font-weight: 500;
|
|
line-height: 1;
|
|
color: color-mix(in srgb, var(--dt-foreground) 82%, transparent);
|
|
background: color-mix(in srgb, var(--ui-bg-elevated) 70%, transparent);
|
|
border: 1px solid var(--ui-stroke-secondary);
|
|
box-shadow: inset 0 -1px 0 color-mix(in srgb, var(--ui-stroke-tertiary) 50%, transparent);
|
|
}
|
|
|
|
/* Unbound slot: a hollow dashed chip inviting a binding. */
|
|
.kbd-cap--ghost {
|
|
color: color-mix(in srgb, var(--dt-foreground) 42%, transparent);
|
|
background: none;
|
|
border-style: dashed;
|
|
border-color: var(--ui-stroke-tertiary);
|
|
box-shadow: none;
|
|
font-style: italic;
|
|
}
|
|
|
|
/* Waiting for a keypress: solid accent, no motion. */
|
|
.kbd-capturing {
|
|
color: var(--theme-primary);
|
|
border-color: color-mix(in srgb, var(--theme-primary) 55%, var(--ui-stroke-secondary)) !important;
|
|
border-style: solid;
|
|
background: color-mix(in srgb, var(--theme-primary) 9%, var(--ui-bg-elevated));
|
|
box-shadow: none;
|
|
}
|