Files
hermes-agent/apps/desktop/src/styles.css
stremtec 0caa23788f fix(desktop): prevent IME Enter from splitting messages and viewport resize from disarming scroll anchor (#38333)
* fix(desktop): prevent IME Enter from splitting messages and viewport resize from disarming scroll anchor

Two fixes for the Hermes Desktop composer:

1. IME composition Enter was treated as message submission. When a Korean/
   Japanese/Chinese IME is composing text and the user presses Enter to
   finalise the preedit, handleEditorKeyDown fired submitDraft() because it
   did not check event.nativeEvent.isComposing. The assistant-ui hidden
   textarea already guards this correctly; the custom contentEditable
   handler was missing it. Added an early return when isComposing is true.

2. Viewport resize (composer expand/collapse, window resize) was disarming
   the scroll sticky-bottom anchor. When the composer grows, the thread
   viewport shrinks, the browser adjusts scrollTop down to keep content
   visible, and the onScroll handler misread this as a user scroll-up.
   Added lastClientHeightRef tracking so the disarm condition now requires
   BOTH stable scrollHeight AND stable clientHeight before treating a
   scrollTop decrease as user intent.

Fixes: random mid-message sends during IME typing; scroll jumps when the
composer resizes or the window changes size.

* fix(desktop): prevent virtualizer measurement adjustments from fighting scroll anchoring

The virtualizer's measureElement callbacks trigger scroll adjustments when
item sizes differ from estimates. These fight our ResizeObserver +
pinToBottom loop, creating visible rubber-banding (view snaps to composer
then jumps back up), even during idle.

Three changes:
1. React.memo on VirtualizedThread to stop parent re-renders cascading
2. Shared stickyBottomRef so scrollToFn can check bottom state
3. scrollToFn override: skip adjustments when user is at bottom

* fix(desktop): use stable useCallback ref instead of inline arrow for onBranchInNewChat

The inline arrow `messageId => void branchInNewChat(messageId)` created a
new function reference on every render. This cascaded through:
  desktop-controller → ChatView → Thread → useMemo([...onBranchInNewChat])
→ new messageComponents object → VirtualizedThread receives new prop
→ React.memo overridden → virtualizer recalculates → measurement
adjustments trigger scroll jumps at the 15-second useStatusSnapshot
interval.

Pass the already-useCallback'd branchInNewChat directly.

* fix(desktop): use ctrlEnter submitMode on hidden textarea + gate ResizeObserver on isRunning

Two root-cause fixes:

1. IME message splitting: The hidden ComposerPrimitive.Input textarea had
   submitMode='enter' (default), so any Enter keydown it received — even
   during IME composition — triggered form.requestSubmit(). Changed to
   submitMode='ctrlEnter' so only the contentEditable div (which correctly
   checks isComposing) handles plain-Enter submission.

2. Scroll jumps during idle: The ResizeObserver auto-follow loop was
   active even when the thread wasn't running, causing spurious
   pinToBottom calls whenever any layout shift occurred (browser reflow,
   font load, GPU cache eviction). Gated the ResizeObserver on
   thread.isRunning so auto-scroll only follows during active streaming.
   User messages still pin via useLayoutEffect, and thread.runStart still
   calls jumpToBottom.

* fix(desktop): keep chat bottom anchor stable through idle layout shifts

* fix(desktop): prevent code block shrink scroll bounce

* fix(desktop): release bottom height lock on run completion

* fix(desktop): keep streaming code blocks rendered

* fix(desktop): keep bottom anchored through final render

* fix(desktop): render streaming reasoning code blocks

* feat(desktop): add subtle streaming block animations
2026-06-03 20:14:52 +00:00

1101 lines
35 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-ink: var(--dt-foreground);
--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);
--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-header:
0 0.0625rem 0 color-mix(in srgb, var(--dt-foreground) 7%, transparent),
0 0.625rem 1.5rem -1.25rem color-mix(in srgb, #000 16%, transparent);
--shadow-composer: 0 0.0625rem 0.125rem color-mix(in srgb, #000 5%, transparent);
--shadow-composer-focus:
0 0 0 0.125rem color-mix(in srgb, var(--dt-composer-ring) calc(10% * var(--composer-ring-strength)), transparent),
0 0 0 0.0625rem color-mix(in srgb, var(--dt-composer-ring) calc(22% * var(--composer-ring-strength)), transparent),
0 0.25rem 0.875rem color-mix(in srgb, #000 8%, transparent),
0 0.75rem 2rem -1.25rem color-mix(in srgb, #000 14%, 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;
--dt-font-mono: 'Cascadia Code', 'JetBrains Mono', 'SF Mono', ui-monospace, Menlo, Consolas, monospace;
--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: var(--conversation-text-font-size);
--conversation-caption-font-size: 0.75rem;
--conversation-line-height: 1.125rem;
--conversation-caption-line-height: 1rem;
--conversation-turn-gap: 0.375rem;
--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;
}
@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);
}
.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;
}
@media (prefers-reduced-motion: reduce) {
.arc-border::before {
animation: none;
}
}
button {
-webkit-app-region: no-drag;
}
[data-slot='button'] {
box-shadow: none;
transition-duration: 100ms;
}
[data-slot='button'][data-variant='outline'],
[data-slot='button'][data-variant='secondary'] {
border-color: var(--ui-stroke-secondary);
background: var(--ui-bg-tertiary);
color: var(--ui-text-primary);
}
[data-slot='button'][data-variant='ghost'] {
color: var(--ui-text-secondary);
}
[data-slot='button'][data-variant='outline']:hover,
[data-slot='button'][data-variant='secondary']:hover,
[data-slot='button'][data-variant='ghost']:hover {
background: var(--chrome-action-hover);
color: var(--ui-text-primary);
}
[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: var(--shadow-composer);
transition:
background-color 200ms ease-out,
border-color 200ms ease-out,
box-shadow 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: var(--shadow-composer-focus);
outline: none;
}
.desktop-input-chrome[aria-invalid='true'] {
border-color: var(--dt-destructive);
}
.desktop-input-chrome[aria-invalid='true']:focus {
box-shadow:
0 0 0 0.125rem color-mix(in srgb, var(--dt-destructive) 18%, transparent),
0 0 0 0.0625rem color-mix(in srgb, var(--dt-destructive) 34%, transparent),
0 0.1875rem 0.625rem color-mix(in srgb, var(--dt-destructive) 12%, transparent);
}
@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;
}
/* Streamed prose hangs slightly indented from the tool/todo column so the
reading column reads as a "reply" within the conversation gutter. Tools,
todos, and thinking blocks keep the existing --message-text-indent so they
remain flush with the user message text above them. */
[data-slot='aui_assistant-message-content'] > .aui-md {
padding-inline-start: var(--md-text-indent, 0.5rem);
}
[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 while you read the response stuck
beneath it. The clamp lifts on hover / focus (clicking the bubble opens the
edit composer, which already shows the full text). --human-msg-full is the
measured content height (set in UserMessage) so expand/collapse animates to
the real height instead of overshooting the cap. */
.sticky-human-clamp {
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:hover .sticky-human-clamp,
.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 {
display: flex;
font-size: var(--fit-text-min, 1rem);
container-type: inline-size;
--captured-length: initial;
--support-sentinel: var(--captured-length, 9999px);
}
.fit-text > [aria-hidden='true'] {
visibility: hidden;
}
.fit-text > :not([aria-hidden='true']) {
flex-grow: 1;
container-type: inline-size;
--captured-length: 100cqi;
--available-space: var(--captured-length);
}
.fit-text > :not([aria-hidden='true']) > * {
display: block;
inline-size: var(--available-space);
line-height: var(--fit-text-line-height, 1);
--support-sentinel: inherit;
--captured-length: 100cqi;
--ratio: tan(atan2(var(--available-space), var(--available-space) - var(--captured-length)));
--font-size: clamp(
var(--fit-text-min, 1em),
1em * var(--ratio),
var(--fit-text-max, infinity * 1px) - var(--support-sentinel)
);
font-size: var(--font-size);
}
@container (inline-size > 0) {
.fit-text > :not([aria-hidden='true']) > * {
white-space: nowrap;
}
}
@property --captured-length {
syntax: '<length>';
initial-value: 0px;
inherits: true;
}
@property --captured-length2 {
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;
box-shadow: var(--shadow-composer) !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: 0.375rem !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'] {
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. Consecutive tool calls stay tight so a step
sequence reads as one action group; the gap between any scaffolding
block and adjacent prose bumps up so the model's reply visually
separates from its scaffolding. */
[data-slot='tool-block'] + [data-slot='tool-block'] {
margin-top: 0.375rem;
}
[data-slot='tool-block']:has(> :nth-child(2)) + [data-slot='tool-block'] {
margin-top: 0.625rem;
}
[data-slot='aui_assistant-message-content']
: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: 1rem;
}
[data-slot='aui_assistant-message-content'] [data-slot='aui_thinking-disclosure'] + [data-slot='tool-block'],
[data-slot='aui_assistant-message-content'] [data-slot='tool-block'] + [data-slot='aui_thinking-disclosure'] {
margin-top: 0.75rem;
}
[data-slot='aui_assistant-message-content'] > [data-slot='tool-block']:first-child {
margin-top: 0;
}
/* 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;
}
}