Files
kandev/apps/web/app/globals.css
T
José Almeida 046ac5c1b6 feat(web): add Ctrl+F search to session, plan, and terminal panels (#686)
* feat(backend): add session-scoped message search endpoint

Adds SearchMessages across repository/service/handler layers and a new
WebSocket action "message.search". Returns lightweight hits with a
60-char snippet around the first match. Session-scoped only: results
never leak across sessions. Uses dialect-aware LIKE/ILIKE so it works
on SQLite today and Postgres later.

* feat(web): add shared panel search bar and Ctrl+F hook

Introduces the reusable PanelSearchBar overlay and usePanelSearch hook
used by the per-panel search features. The hook listens on the window
and fires only when focus is within the panel root, preventDefaults the
browser find-in-page, and wires Esc to close. PanelRoot becomes a
forwardRef so panel components can expose their root element to the
hook. Adds SHORTCUTS.FIND_IN_PANEL plus shared highlight/flash styles.

* feat(web): add Ctrl+F search to terminal panels

Mounts the xterm addon-search on both passthrough and shell terminals,
exposing next/prev plus case-sensitivity and regex toggles. Invalid
regex surfaces an inline error without firing the search. Because xterm
consumes keystrokes before React, the Ctrl/Cmd+F intercept is added to
attachCustomKeyEventHandler so the shortcut opens the bar from inside
the terminal. Results counter comes from onDidChangeResults; decorations
clear on close.

* feat(web): add Ctrl+F search to plan panel

Adds a custom TipTap extension that builds a ProseMirror DecorationSet
over the document and exposes setPlanSearchQuery / planSearchNext /
planSearchPrev / clearPlanSearch commands. The current match gets an
extra class and scrolls into view. Match count is read from the plugin
state via transaction subscription, so edits to the document update the
counter automatically. Find-only — no replace.

* feat(web): add Ctrl+F search to session chat panel

Wires the new backend message.search action into a debounced client
hook and renders a hits dropdown under the search bar, with the query
highlighted in each snippet. Clicking a hit scrolls the message into
view and flashes it. When the target message is older than what is
currently loaded, the hook loops through loadOlder pages until the
message renders, making search work across lazy-loaded history without
needing to scroll-and-wait manually.

* fix(web): lazy-load xterm addon-search to avoid SSR crash

addon-search is published as a UMD bundle that references `self` at
module load. Statically importing it breaks the Next.js static
prerender of /. Deferring the import to an effect keeps the module out
of the SSR graph without changing runtime behaviour.

* fix(web): propagate plan editor via state so plan-search hook re-runs

The plan-search hook subscribed to editor transactions inside a
useEffect that depended on a ref object. Because ref.current changes do
not trigger effect re-runs, if the Editor instance was not yet ready
when the hook first mounted, the transaction handler never attached and
the match counter stayed at 0 / 0. Tracking the editor via state (in
addition to the ref) causes the deps to change when the Editor becomes
available, so the handler attaches and the counter updates.

* feat(web): allow overriding message list renderer via ?renderer= query

Exposes the "native" / "virtuoso" choice at runtime so E2E coverage can
exercise both paths without a redeploy or code change. Defaults to the
existing "native" strategy when no override is present.

* test(web): add Playwright coverage for panel-scoped Ctrl+F search

Adds an e2e/tests/search/ suite with five specs:

- panel-search-shared: cross-panel contract (Ctrl+F opens, autofocus,
  Esc closes, close button, state reset on reopen) parametrised over
  session, plan, and terminal.
- terminal-search: xterm addon-search integration — key intercept
  without leaking "f" to the PTY, case + regex toggles (incl. invalid
  regex error), Esc does not reach the shell, backspace adjusts count.
- plan-search: TipTap plugin — highlight count, exactly one current
  match, counter, Enter / Shift+Enter wrapping, close clears
  decorations, no case/regex toggles in plan bar.
- session-search: WebSocket-backed hits list, "No matches" state,
  click-to-flash, cross-session isolation, close, and virtuoso renderer
  coverage via the new ?renderer= override.
- panel-focus-routing: bar mounts inside the focused panel only.

Three tests capture GIFs for the PR description via prCapture when
CAPTURE_PR_ASSETS is set (one per panel). Helpers live in
e2e/helpers/panel-search.ts and e2e/helpers/seed-session-messages.ts;
SessionPage gains search-bar locators.

* fix(web): prevent plan-search update loop on spurious editor transactions

TipTap fires `transaction` on every cursor move/click. The match-info
handler allocated a fresh `{current, total}` object each time, so React
always saw new identity, re-rendered, and could cascade with downstream
effects (e.g. `rehydrateCommentMarks`) into a "Maximum update depth
exceeded" loop. Use a functional setState with equality bail-out so the
handler only schedules a render when the counter actually changes.

* fix(web): retry terminal search once xterm addon-search finishes loading

The SearchAddon is dynamic-imported and was stored in a ref. The
query→findNext effect called `addonRef.current?.findNext(...)` — a
silent no-op if the addon wasn't loaded yet — and its deps didn't
include addon readiness, so a query typed before the import resolved
was dropped forever. Track the addon via state so the search effect
re-runs when it becomes available (and re-binds cleanly if the terminal
is ever re-created). Also deduplicate match-info updates via functional
setState + equality check.

* fix(web): route Ctrl+F to the last-clicked panel when focus is ambient

The chat panel's message list has no focus-capturing child, so clicking
on a message left `document.activeElement === <body>` and the existing
`isFocusWithin` gate silently fell through to the browser's native find.
Fix with two complementary mechanisms:

- `usePanelSearch`: track the most-recently-clicked panel via a
  window-level pointerdown capture listener. When focus is ambient
  (body/html), that panel claims Ctrl+F. Window-level capture dodges
  any descendant `stopPropagation` that would skip a container-level
  listener.
- Chat panel root: tabIndex=-1 + onMouseDown that focuses the root when
  the click target isn't already focusable. Normal path matches how
  plan/terminal already work (TipTap/xterm capture focus on click).

* feat(web): show agent profile name and logo in session search hits

The hit list showed uppercase "AGENT"/"USER" with vivid blue/green
colors that clashed with the app's muted metadata aesthetic. Replace
with the actual author identity:

- Agent hits render the profile name (falling back to "Agent") with
  the profile's <AgentLogo> inline — same mechanism session tabs use.
- User hits render "You" with a subtle `text-primary/80` tint that
  echoes the user bubble's `bg-primary/30`, keeping a light color
  differentiator without reintroducing loud blue/green.

Added `useSessionAgentIdentity` (plus a `useSessionAgentProfile` helper)
that reads from the store via two primitive selectors — the id, then
the profile — and composes the final object inside `useMemo`. A naive
single selector returning `{label, name}` triggers the "getSnapshot
should be cached" infinite loop in `useSyncExternalStore`.

* style: apply prettier formatting to search files

* fix(web): inline resolveStrategy in useMemo to satisfy react-hooks/use-memo

* fix(backend): escape LIKE metacharacters in message search

User query of `%` previously built LIKE pattern `%%` which matched every
message in the session; `file_name` also matched `filename`, `file name`,
etc. Escape the three LIKE metacharacters (`\`, `%`, `_`) before wrapping
the pattern, and declare `ESCAPE '\'` on the clause. Add regression tests
covering wildcard literals and the literal-percent happy path.

* fix(backend): slice search snippet in rune space to keep UTF-8 valid

`buildSnippet` indexed and sliced on byte offsets, so content containing
multi-byte characters (emoji, CJK, accented letters) could be cut
mid-rune — `encoding/json` then replaced the invalid bytes with `\ufffd`
in the websocket response. Switch to rune slicing and per-rune
case-folding via `unicode.ToLower`; this also avoids the
`strings.ToLower` byte-length-change pitfall (e.g. Turkish İ, German ß).
Add a handler test covering emoji, CJK, and large multi-byte content.

* fix(web): cancel backfill loop when user clicks a newer session hit

`setActiveHit` awaits up to 40 `loadOlder()` iterations when the target
message isn't in the DOM. If the user clicked a second hit while those
awaits were in flight, the in-progress loop kept running and eventually
scrolled to the original target, overriding the user's intent. Gate the
loop on a generation counter so only the most recent call's backfill
drives the scroll.

* fix(web): gate plan-search scroll side-effect on dispatch

TipTap invokes commands with `dispatch === undefined` during `can()`
checks to test applicability (e.g. before enabling a menu item). The
next/prev commands called `scrollToMatch` outside the `if (dispatch)`
guard, so a dry-run invocation would scroll the editor to a match the
user didn't navigate to. Move the scroll inside the dispatch block.

* fix(web): address review nits on search UX

- formatTime now checks Number.isNaN(d.getTime()) before formatting;
  Invalid Date doesn't throw, so the catch branch was unreachable and
  hits with a malformed created_at rendered the literal "Invalid Date".
- prefers-reduced-motion now silences the .search-flash animation
  alongside the other custom keyframe animations.
- planScript escapes `title` symmetrically with `content` so callers
  passing arbitrary strings don't produce malformed JSON.

* fix(web): fold description into opts to keep seedTask under param limit

`seedTask` had 6 parameters, violating the repo's 5-parameter cap and
failing CI lint. Fold `description` into the existing `opts` object and
update all callsites.

* test(web): add unit tests for useSessionSearch + remove redundant ternary

Cover the non-trivial hook logic called out in review: debounced fetch
coalescing, request-ID cancellation for superseded queries, null
sessionId short-circuit, and close() resets. Also simplify the
`matches.length ? 0 : 0` ternary flagged as a dead branch — both sides
were already 0.

* fix(web): exclude PanelRoot (tabindex=-1) from the focus short-circuit

`target.closest("[tabindex]")` was matching PanelRoot itself (which has
`tabIndex={-1}` so it can receive programmatic focus), causing the guard
to early-return and the `panelRef.focus()` call to never fire — defeating
the Ctrl+F focus-routing that this handler was added for.

Narrow the selector to `[tabindex]:not([tabindex='-1'])` so it only
short-circuits on genuinely interactive elements.

* fix(web): read isReadOnlyMode from live ref in shell-terminal key handler

`attachCustomKeyEventHandler` is called once and has no dispose hook, so
the inner handler keeps its initial closure forever. Other values
(sessionId, send, onFindInPanel) are read via `stateRef` so they stay
current, but `isReadOnlyMode` was captured by closure — if the prop ever
toggled mid-session, the Cmd+Arrow→Home/End path would continue firing
against a read-only terminal. Include it in the state ref for
consistency.

* fix(web): flatten doubly-nested seedTask opts in virtuoso spec; drop unused MODIFIER

The regex-based transform in 7152b936 over-nested one call in
session-search.spec.ts — `opts.description` received an object rather
than a string, so the virtuoso renderer test never received a proper
task description. Flatten it.

Also remove MODIFIER imports/declarations in terminal-search.spec.ts and
panel-search-shared.spec.ts that were left dangling behind `void` suppressors.

* fix(web): abort session-search backfill loop on close/unmount

Move reset logic into `close()` so the lifecycle is explicit (and avoids
the set-state-in-effect lint path the previous reset effect triggered).
Bump the `activeHitGenRef` generation counter on both `close()` and
unmount so an in-flight `setActiveHit` backfill can no longer issue
`loadOlder()` calls for a hidden UI.

Also extract `useDebouncedSearch`, `useSetActiveHit`, and a
`focusMessageElement` helper to keep `useSessionSearch` under the
100-line function cap.

* perf(web): skip PanelSearchBar debounce for session search

Session search already debounces the WS call inside `useDebouncedSearch`
(180ms), but the shared bar adds its own 150ms debounce on top — giving
a 330ms keystroke→request latency instead of the intended ~180ms.
Pass `debounceMs={0}` so the bar forwards queries immediately and the
hook-level debounce is the single source of timing truth.
2026-04-24 17:14:03 +01:00

1395 lines
34 KiB
CSS

@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@import "@kandev/theme/fonts.css";
@import "dockview-react/dist/styles/dockview.css";
@import "./dockview-theme.css";
@source "../components/**/*.{js,ts,jsx,tsx}";
@source "../../packages/ui/src/**/*.{js,ts,jsx,tsx}";
@custom-variant dark (&:is(.dark *));
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-mono:
"Monaspace Neon", "Geist Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
--font-mono-size: 12px;
--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);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--radius-sm: var(--radius);
--radius-md: var(--radius);
--radius-lg: var(--radius);
--radius-xl: var(--radius);
--radius-2xl: var(--radius);
--radius-3xl: var(--radius);
--radius-4xl: var(--radius);
}
:root {
--font-geist-sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--font-sans: "Figtree", "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--font-geist-mono:
"Geist Mono", ui-monospace, "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace;
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.51 0.23 277);
--primary-foreground: oklch(0.96 0.02 272);
--secondary: oklch(0.967 0.001 286.375);
--secondary-foreground: oklch(0.21 0.006 285.885);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.68 0.18 276);
--accent-foreground: oklch(0.98 0 0);
--destructive: oklch(0.58 0.22 27);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.708 0 0);
--chart-1: oklch(0.79 0.1 275);
--chart-2: oklch(0.68 0.16 277);
--chart-3: oklch(0.59 0.2 277);
--chart-4: oklch(0.51 0.23 277);
--chart-5: oklch(0.46 0.21 277);
--radius: 0.375rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.51 0.23 277);
--sidebar-primary-foreground: oklch(0.96 0.02 272);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--scrollbar-thumb: oklch(0.78 0 0);
--scrollbar-thumb-hover: oklch(0.66 0 0);
--scrollbar-track: var(--background);
--sidebar-ring: oklch(0.708 0 0);
/* Git diff colors - centralized for easy maintenance */
--git-addition: 63 185 80; /* #3fb950 - GitHub addition green */
--git-deletion: 244 112 103; /* #f47067 - GitHub deletion red */
}
.dark {
/* Grey scale: #181818 bg → #1F1F1F card → #2a2a2a borders → #333 hover */
--background: #181818;
--foreground: #d4d4d4;
--card: #1f1f1f;
--card-foreground: #d4d4d4;
--popover: #222222;
--popover-foreground: #d4d4d4;
--primary: oklch(0.59 0.2 277);
--primary-foreground: oklch(0.96 0.02 272);
--secondary: #2a2a2a;
--secondary-foreground: #d4d4d4;
--muted: #2a2a2a;
--muted-foreground: #888888;
--accent: oklch(0.62 0.18 276);
--accent-foreground: #d4d4d4;
--destructive: oklch(0.704 0.191 22.216);
--border: #2a2a2a;
--input: #2a2a2a;
--ring: #555555;
--chart-1: oklch(0.79 0.1 275);
--chart-2: oklch(0.68 0.16 277);
--chart-3: oklch(0.59 0.2 277);
--chart-4: oklch(0.51 0.23 277);
--chart-5: oklch(0.46 0.21 277);
--sidebar: #171717;
--sidebar-foreground: #d4d4d4;
--sidebar-primary: oklch(0.68 0.16 277);
--sidebar-primary-foreground: oklch(0.96 0.02 272);
--sidebar-accent: #2a2a2a;
--sidebar-accent-foreground: #d4d4d4;
--sidebar-border: #262626;
--scrollbar-thumb: rgba(100, 100, 100, 0.5);
--scrollbar-thumb-hover: rgba(130, 130, 130, 0.6);
--scrollbar-track: var(--background);
--sidebar-ring: #555555;
/* Git diff colors - same as light mode for consistency */
--git-addition: 63 185 80; /* #3fb950 - GitHub addition green */
--git-deletion: 244 112 103; /* #f47067 - GitHub deletion red */
}
@layer base {
* {
@apply border-border outline-ring/50;
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) transparent;
}
body {
@apply bg-background text-foreground;
}
}
*::-webkit-scrollbar {
width: 10px;
height: 10px;
}
*::-webkit-scrollbar-track {
background: var(--scrollbar-track);
border-radius: 999px;
}
*::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: 999px;
border: 2px solid transparent;
background-clip: padding-box;
}
*::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-hover);
background-clip: padding-box;
}
/* xterm.js scrollbar styling */
.xterm .xterm-scrollable-element > .scrollbar {
width: 10px !important;
right: 0 !important;
background: transparent !important;
}
.xterm .xterm-scrollable-element > .scrollbar > .slider {
background: var(--scrollbar-thumb) !important;
border-radius: 999px !important;
width: 6px !important;
left: 2px !important;
}
.xterm .xterm-scrollable-element > .scrollbar:hover > .slider {
background: var(--scrollbar-thumb-hover) !important;
}
@layer utilities {
.overflow-wrap-anywhere {
overflow-wrap: anywhere;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
/* Thin scrollbar for menus */
.scrollbar-thin {
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) transparent;
}
.scrollbar-thin::-webkit-scrollbar {
width: 8px;
}
.scrollbar-thin::-webkit-scrollbar-track {
background: transparent !important;
}
.scrollbar-thin::-webkit-scrollbar-thumb {
background: var(--scrollbar-thumb);
border-radius: 4px;
}
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
background: var(--scrollbar-thumb-hover);
}
/* Safe area padding for iPhone notch and home indicator */
.pb-safe {
padding-bottom: env(safe-area-inset-bottom, 0px);
}
.pt-safe {
padding-top: env(safe-area-inset-top, 0px);
}
.pl-safe {
padding-left: env(safe-area-inset-left, 0px);
}
.pr-safe {
padding-right: env(safe-area-inset-right, 0px);
}
/* Safe area height for spacer elements */
.h-safe {
height: env(safe-area-inset-bottom, 0px);
}
}
/* Grid Spinner - 3x3 cube animation */
@keyframes spinner-grid {
0%,
70%,
100% {
transform: scale3d(0.5, 0.5, 1);
}
35% {
transform: scale3d(0, 0, 1);
}
}
.spinner-grid {
display: inline-grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.15em;
width: 1em;
height: 1em;
}
.spinner-grid-cube {
width: 100%;
height: 100%;
background-color: currentColor;
animation: spinner-grid 1.3s ease-in-out infinite;
}
.spinner-grid-cube:nth-child(1) {
animation-delay: 0.2s;
}
.spinner-grid-cube:nth-child(2) {
animation-delay: 0.3s;
}
.spinner-grid-cube:nth-child(3) {
animation-delay: 0.4s;
}
.spinner-grid-cube:nth-child(4) {
animation-delay: 0.1s;
}
.spinner-grid-cube:nth-child(5) {
animation-delay: 0.2s;
}
.spinner-grid-cube:nth-child(6) {
animation-delay: 0.3s;
}
.spinner-grid-cube:nth-child(7) {
animation-delay: 0s;
}
.spinner-grid-cube:nth-child(8) {
animation-delay: 0.1s;
}
.spinner-grid-cube:nth-child(9) {
animation-delay: 0.2s;
}
@keyframes border-flash-glow {
0%,
100% {
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--primary) 20%, transparent);
}
50% {
box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--primary) 60%, transparent);
}
}
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
@keyframes shimmer-vertical {
0% {
transform: translateY(-100%);
}
100% {
transform: translateY(100%);
}
}
.animate-shimmer {
animation: shimmer 1.5s ease-in-out infinite;
}
.animate-shimmer-vertical {
animation: shimmer-vertical 1.5s ease-in-out infinite;
}
@keyframes changes-tab-flash {
0% {
background-color: transparent;
}
25% {
background-color: color-mix(in oklab, var(--primary) 25%, transparent);
}
100% {
background-color: transparent;
}
}
.animate-changes-flash {
animation: changes-tab-flash 1s ease-out;
border-radius: 4px;
}
@keyframes chat-input-running {
0%,
100% {
border-color: color-mix(in oklab, var(--primary) 40%, var(--border));
box-shadow:
0 0 6px 1px color-mix(in oklab, var(--primary) 12%, transparent),
0 0 15px 3px color-mix(in oklab, var(--primary) 6%, transparent);
}
50% {
border-color: color-mix(in oklab, var(--primary) 70%, var(--border));
box-shadow:
0 0 20px 4px color-mix(in oklab, var(--primary) 20%, transparent),
0 0 50px 8px color-mix(in oklab, var(--primary) 10%, transparent);
}
}
.chat-input-running {
animation: chat-input-running 3s ease-in-out infinite;
}
@keyframes chat-input-running-plan {
0%,
100% {
border-color: color-mix(in oklab, oklch(0.65 0.2 300) 40%, var(--border));
box-shadow:
0 0 6px 1px color-mix(in oklab, oklch(0.65 0.2 300) 12%, transparent),
0 0 15px 3px color-mix(in oklab, oklch(0.65 0.2 300) 6%, transparent);
}
50% {
border-color: color-mix(in oklab, oklch(0.65 0.2 300) 70%, var(--border));
box-shadow:
0 0 20px 4px color-mix(in oklab, oklch(0.65 0.2 300) 20%, transparent),
0 0 50px 8px color-mix(in oklab, oklch(0.65 0.2 300) 10%, transparent);
}
}
.chat-input-running-plan {
animation: chat-input-running-plan 3s ease-in-out infinite;
}
@keyframes chat-input-starting {
0%,
100% {
border-color: color-mix(in oklab, var(--primary) 40%, var(--border));
box-shadow:
0 0 4px 1px color-mix(in oklab, var(--primary) 10%, transparent),
0 0 10px 2px color-mix(in oklab, var(--primary) 5%, transparent);
}
50% {
border-color: color-mix(in oklab, var(--primary) 60%, var(--border));
box-shadow:
0 0 12px 3px color-mix(in oklab, var(--primary) 15%, transparent),
0 0 30px 6px color-mix(in oklab, var(--primary) 8%, transparent);
}
}
.chat-input-starting {
animation: chat-input-starting 2s ease-in-out infinite;
}
@media (prefers-reduced-motion: reduce) {
.animate-changes-flash,
.chat-input-running,
.chat-input-running-plan,
.chat-input-starting,
.search-flash {
animation: none;
}
}
@layer components {
.border-flash-animation {
position: relative;
}
.border-flash-animation::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
z-index: 10;
animation: border-flash-glow 3s ease-in-out infinite;
}
}
.cm-tooltip {
border: 1px solid var(--border) !important;
background: var(--popover, oklch(0.205 0 0)) !important;
color: var(--popover-foreground, oklch(0.985 0 0)) !important;
border-radius: calc(var(--radius) + 2px);
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);
padding: 8px;
}
.cm-tooltip-autocomplete {
padding: 0;
background: var(--popover, oklch(0.205 0 0)) !important;
}
.cm-tooltip-autocomplete ul {
list-style: none;
margin: 0;
padding: 12px;
max-height: 240px;
overflow: auto;
}
.cm-tooltip-autocomplete li {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 12px;
border-radius: 6px;
cursor: pointer;
font-size: 0.875rem;
color: var(--popover-foreground, oklch(0.985 0 0));
}
.cm-tooltip-autocomplete li[aria-selected="true"] {
background: color-mix(in oklab, var(--primary) 20%, var(--popover)) !important;
color: var(--primary-foreground) !important;
border-radius: 0;
}
.cm-tooltip-autocomplete li[aria-selected="true"]:first-child {
border-top-left-radius: 6px;
border-top-right-radius: 6px;
}
.cm-tooltip-autocomplete li[aria-selected="true"]:last-child {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
}
.cm-completionLabel {
font-weight: 500;
}
.cm-completionDetail {
margin-left: auto;
font-size: 0.875rem;
color: var(--muted-foreground) !important;
}
.cm-completionMatchedText {
text-decoration: none;
color: var(--primary) !important;
}
.cm-tooltip-autocomplete li[aria-selected="true"] .cm-completionMatchedText {
color: var(--primary-foreground);
}
.cm-completionIcon {
opacity: 0.65;
}
.cm-completionIcon {
width: 14px;
height: 14px;
display: inline-block;
margin-right: 6px;
color: var(--muted-foreground);
background-color: currentColor;
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
-webkit-mask-size: contain;
-webkit-mask-repeat: no-repeat;
-webkit-mask-position: center;
}
.cm-completionIcon-slash {
mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M7 9a2 2 0 1 1 2 -2v10a2 2 0 1 1 -2 -2h10a2 2 0 1 1 -2 2v-10a2 2 0 1 1 2 2h-10'/></svg>");
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M7 9a2 2 0 1 1 2 -2v10a2 2 0 1 1 -2 -2h10a2 2 0 1 1 -2 2v-10a2 2 0 1 1 2 2h-10'/></svg>");
}
.cm-completionIcon-mention {
mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M8 12a4 4 0 1 0 8 0a4 4 0 1 0 -8 0'/><path d='M16 12v1.5a2.5 2.5 0 0 0 5 0v-1.5a9 9 0 1 0 -5.5 8.28'/></svg>");
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M8 12a4 4 0 1 0 8 0a4 4 0 1 0 -8 0'/><path d='M16 12v1.5a2.5 2.5 0 0 0 5 0v-1.5a9 9 0 1 0 -5.5 8.28'/></svg>");
}
.cm-completionIcon-prompt {
display: none !important;
width: 0 !important;
margin-right: 0 !important;
background: none !important;
}
.cm-tooltip-autocomplete li[aria-selected="true"] .cm-completionIcon {
color: var(--primary-foreground);
}
.task-chat-editor .cm-placeholder {
color: var(--muted-foreground) !important;
}
.task-chat-editor .cm-editor {
background: transparent !important;
box-shadow: none !important;
outline: none !important;
}
.task-chat-editor .cm-editor.cm-focused {
outline: none !important;
}
.task-chat-editor .cm-scroller,
.task-chat-editor .cm-content,
.task-chat-editor .cm-line,
.task-chat-editor .cm-line span {
background: transparent !important;
background-color: transparent !important;
color: var(--foreground) !important;
}
.task-chat-editor .cm-cursor {
border-left-color: var(--foreground);
}
.task-chat-editor .cm-selectionBackground {
background: color-mix(in oklab, var(--primary) 25%, var(--background)) !important;
}
.task-chat-editor .cm-content ::selection {
background: color-mix(in oklab, var(--primary) 30%, var(--background));
}
/* Legacy Milkdown styles removed - plan editor now uses TipTap */
/* ── Shared markdown body styles ──
Single source of truth for rendered markdown across TipTap (plan editor)
and ReactMarkdown (chat, PR, changelog, etc.). */
.markdown-body {
font-size: 13px;
line-height: 1.625;
color: var(--foreground);
overflow-wrap: anywhere;
font-family:
-apple-system, "system-ui", "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji";
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5 {
font-weight: 700;
color: var(--foreground);
margin-top: 1.25rem;
margin-bottom: 0.375rem;
line-height: 1.3;
}
.markdown-body :is(h1, h2, h3, h4, h5):first-child {
margin-top: 0;
}
.markdown-body h1 {
font-size: 1.25rem;
}
.markdown-body h2 {
font-size: 1.0625rem;
}
.markdown-body h3 {
font-size: 0.9375rem;
}
.markdown-body p {
line-height: 1.625;
}
.markdown-body p + p {
margin-top: 0.5em;
}
.markdown-body pre {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.markdown-body ul {
list-style-type: disc;
padding-left: 1.25rem;
margin-top: 1rem;
margin-bottom: 1rem;
}
.markdown-body ol {
list-style-type: decimal;
padding-left: 1.25rem;
margin-top: 1rem;
margin-bottom: 1rem;
}
.markdown-body li {
margin-top: 0.125rem;
margin-bottom: 0.125rem;
}
.markdown-body li > ul,
.markdown-body li > ol {
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}
/* Inline code — accent (indigo) color */
.markdown-body :not(pre) > code,
.markdown-body code:not(pre code) {
background: var(--muted);
color: var(--accent);
padding: 0.1rem 0.375rem;
border-radius: 0.25rem;
font-size: 0.9em;
font-family: var(--font-mono);
}
.markdown-body a {
color: var(--primary);
text-decoration-line: underline;
text-underline-offset: 2px;
}
.markdown-body a:hover {
text-decoration-line: underline;
}
.markdown-body blockquote {
border-left: 3px solid var(--primary);
padding-left: 0.75rem;
margin-left: 0;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
color: var(--muted-foreground);
font-style: italic;
}
.markdown-body hr {
border: none;
border-top: 1px solid var(--border);
margin: 1.5rem 0;
}
.markdown-body table {
border-collapse: collapse;
width: 100%;
margin: 0.75rem 0;
}
.markdown-body th,
.markdown-body td {
border: 1px solid var(--border);
padding: 0.25rem 0.5rem;
font-size: 13px;
}
.markdown-body th {
background: var(--muted);
font-weight: 600;
}
.markdown-body thead {
background: color-mix(in oklch, var(--muted) 50%, transparent);
}
.markdown-body tbody tr {
border-bottom: 1px solid var(--border);
}
.markdown-body tbody tr:last-child {
border-bottom: 0;
}
.markdown-body tbody tr:hover {
background: color-mix(in oklch, var(--muted) 50%, transparent);
}
.markdown-body strong {
color: var(--foreground);
font-weight: 700;
}
/* User message markdown overrides — adjust for primary background */
.markdown-body-user :not(pre) > code,
.markdown-body-user code:not(pre code) {
background: color-mix(in oklch, var(--primary) 20%, transparent);
color: var(--foreground);
}
/* TipTap editor styles */
.tiptap p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: var(--muted-foreground);
pointer-events: none;
height: 0;
}
.tiptap p {
margin: 0;
}
/* Inline code */
.tiptap code {
background-color: var(--muted);
border-radius: 0.25rem;
padding: 0.125rem 0.25rem;
font-size: 0.75rem;
font-family: var(--font-mono, ui-monospace, monospace);
}
/* Code blocks */
.tiptap pre {
background-color: color-mix(in oklch, var(--card) 80%, black 20%);
border: 1px solid var(--border);
border-radius: 0.375rem;
padding: 0.5rem 0.75rem;
margin: 0.25rem 0;
overflow-x: auto;
position: relative;
}
.tiptap pre code {
background: none;
padding: 0;
border-radius: 0;
font-size: 0.75rem;
line-height: 1.4;
font-family: var(--font-mono, ui-monospace, monospace);
}
/* Language selector on code blocks */
.tiptap pre .code-block-language {
position: absolute;
top: 0.25rem;
right: 0.25rem;
font-size: 0.625rem;
font-family: var(--font-sans, ui-sans-serif, sans-serif);
color: var(--muted-foreground);
background: var(--muted);
border: 1px solid var(--border);
border-radius: 0.25rem;
padding: 0.0625rem 0.25rem;
cursor: pointer;
outline: none;
opacity: 0;
transition: opacity 0.15s;
}
.tiptap pre:hover .code-block-language,
.tiptap pre:focus-within .code-block-language {
opacity: 1;
}
/* Lowlight / highlight.js syntax colors */
.tiptap pre code .hljs-comment,
.tiptap pre code .hljs-quote {
color: var(--muted-foreground);
font-style: italic;
}
.tiptap pre code .hljs-keyword,
.tiptap pre code .hljs-selector-tag,
.tiptap pre code .hljs-built_in {
color: oklch(0.65 0.2 300);
}
.tiptap pre code .hljs-string,
.tiptap pre code .hljs-attr,
.tiptap pre code .hljs-addition {
color: oklch(0.7 0.15 150);
}
.tiptap pre code .hljs-number,
.tiptap pre code .hljs-literal {
color: oklch(0.7 0.15 60);
}
.tiptap pre code .hljs-title,
.tiptap pre code .hljs-section,
.tiptap pre code .hljs-type {
color: oklch(0.75 0.15 80);
}
.tiptap pre code .hljs-name,
.tiptap pre code .hljs-tag {
color: oklch(0.65 0.2 20);
}
.tiptap pre code .hljs-variable,
.tiptap pre code .hljs-template-variable {
color: oklch(0.65 0.2 20);
}
.tiptap pre code .hljs-regexp,
.tiptap pre code .hljs-link {
color: oklch(0.7 0.15 30);
}
.tiptap pre code .hljs-symbol,
.tiptap pre code .hljs-bullet {
color: oklch(0.65 0.2 330);
}
.tiptap pre code .hljs-meta {
color: var(--muted-foreground);
}
.tiptap pre code .hljs-deletion {
color: oklch(0.65 0.2 20);
}
.tiptap pre code .hljs-emphasis {
font-style: italic;
}
.tiptap pre code .hljs-strong {
font-weight: bold;
}
/* Monaco Editor - Comment decorations */
.monaco-comment-line {
background-color: rgba(99, 102, 241, 0.12) !important;
}
/* Lines-decoration lane for commented lines — clickable bar */
.monaco-comment-bar {
cursor: pointer !important;
background-color: rgba(99, 102, 241, 0.12) !important;
border-left: 3px solid rgba(99, 102, 241, 0.6) !important;
}
.monaco-comment-bar:hover {
background-color: rgba(99, 102, 241, 0.25) !important;
}
/* Chat bubble icon on the bar (first line of a comment only) */
.monaco-comment-bar-icon {
cursor: pointer !important;
position: relative !important;
background-color: rgba(99, 102, 241, 0.12) !important;
border-left: 3px solid rgba(99, 102, 241, 0.6) !important;
}
.monaco-comment-bar-icon:hover {
background-color: rgba(99, 102, 241, 0.25) !important;
}
.monaco-comment-bar-icon::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 12px;
height: 12px;
background: rgba(99, 102, 241, 0.9);
mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z'/></svg>");
-webkit-mask-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'><path d='M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z'/></svg>");
mask-size: contain;
-webkit-mask-size: contain;
}
/* Line number bg tint for commented lines */
.monaco-comment-line-number {
background-color: rgba(99, 102, 241, 0.12) !important;
}
/* TipTap Plan Editor Styles
Element styles (headings, lists, code, etc.) come from .markdown-body above.
Only TipTap-specific layout, container, and editor behavior styles remain here. */
.tiptap-plan-wrapper {
height: 100%;
overflow: auto;
border-radius: 0;
}
.tiptap-plan-wrapper:not(.dark) {
background: #ffffff;
}
.tiptap-plan-wrapper .tiptap {
height: 100%;
padding: 0.75rem;
outline: none;
cursor: text;
}
.tiptap-plan-wrapper .tiptap:focus {
outline: none;
}
/* Task lists (TipTap-specific data attribute) */
.tiptap-plan-wrapper .tiptap ul[data-type="taskList"] {
list-style: none !important;
padding-left: 0 !important;
}
.tiptap-plan-wrapper .tiptap ul[data-type="taskList"] li {
display: flex;
align-items: flex-start;
gap: 0.5rem;
}
.tiptap-plan-wrapper .tiptap ul[data-type="taskList"] li label {
margin-top: 0.125rem;
}
/* Code blocks (TipTap-specific: lowlight/highlight.js syntax highlighting) */
.tiptap-plan-wrapper .tiptap pre {
background: var(--background) !important;
padding: 0.5rem 0.75rem !important;
border-radius: 0.375rem !important;
overflow-x: auto !important;
margin-top: 0.5rem !important;
margin-bottom: 0.5rem !important;
}
.tiptap-plan-wrapper:not(.dark) .tiptap pre {
background: #f5f5f5 !important;
}
.tiptap-plan-wrapper .tiptap pre code {
background: transparent !important;
color: #d4d4d4;
padding: 0 !important;
font-size: var(--font-mono-size) !important;
font-family: var(--font-mono) !important;
}
.tiptap-plan-wrapper:not(.dark) .tiptap pre code {
color: #1e1e1e;
}
/* Highlight marks */
.tiptap-plan-wrapper mark {
background-color: rgba(255, 212, 0, 0.3);
border-radius: 2px;
padding: 1px 0;
}
/* Comment highlights — inline mark spans (like Highlight extension) */
.tiptap-plan-wrapper .comment-highlight {
background-color: color-mix(in oklch, var(--accent) 70%, transparent);
border-radius: 3px;
border-bottom: 2px solid color-mix(in oklch, var(--accent-foreground) 25%, transparent);
cursor: pointer;
transition: background-color 0.15s ease;
padding: 2px 1px;
box-decoration-break: clone;
-webkit-box-decoration-break: clone;
}
.tiptap-plan-wrapper .comment-highlight:hover {
background-color: color-mix(in oklch, var(--accent) 90%, transparent);
}
/* Comment badge — single widget decoration at end of mark range */
.tiptap-plan-wrapper .comment-badge {
position: relative;
display: inline-block;
width: 0;
height: 0;
overflow: visible;
vertical-align: top;
}
.tiptap-plan-wrapper .comment-badge svg {
position: absolute;
top: -6px;
left: 2px;
width: 11px;
height: 11px;
color: rgba(99, 102, 241, 0.8);
cursor: pointer;
transition: color 0.15s ease;
pointer-events: auto;
}
.tiptap-plan-wrapper .comment-badge:hover svg {
color: rgba(99, 102, 241, 1);
}
/* Drag handle */
.plan-drag-handle {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
margin-left: 4px;
border-radius: 4px;
color: var(--muted-foreground);
opacity: 0;
cursor: grab;
transition:
opacity 0.15s,
background-color 0.15s;
}
.plan-drag-handle:hover {
background-color: var(--muted);
opacity: 1 !important;
}
.tiptap-plan-wrapper:hover .plan-drag-handle {
opacity: 0.5;
}
.plan-drag-handle:active {
cursor: grabbing;
}
/* Mermaid diagram styles */
.tiptap-plan-wrapper .mermaid-container {
position: relative;
width: fit-content;
max-width: 100%;
margin: 0.75rem auto !important;
}
.tiptap-plan-wrapper .mermaid-container svg {
max-width: 100% !important;
height: auto !important;
}
.tiptap-plan-wrapper .mermaid-error {
color: #ef4444 !important;
font-size: 0.75rem !important;
padding: 0.5rem !important;
white-space: pre-wrap !important;
}
.tiptap-plan-wrapper .tiptap pre.mermaid-code-view {
margin: 0 !important;
padding: 0.75rem !important;
font-size: 0.75rem !important;
line-height: 1.6 !important;
white-space: pre-wrap !important;
background: transparent !important;
border: none !important;
}
.tiptap-plan-wrapper .tiptap pre.mermaid-code-view code {
color: var(--foreground) !important;
font-family: var(--font-mono) !important;
font-size: 0.75rem !important;
}
.tiptap-plan-wrapper .tiptap pre.mermaid-code-view .mm-keyword {
color: oklch(0.65 0.2 300);
}
.tiptap-plan-wrapper .tiptap pre.mermaid-code-view .mm-arrow {
color: oklch(0.7 0.15 60);
}
.tiptap-plan-wrapper .tiptap pre.mermaid-code-view .mm-label {
color: oklch(0.7 0.15 150);
}
.tiptap-plan-wrapper .tiptap pre.mermaid-code-view .mm-text {
color: var(--muted-foreground);
}
.tiptap-plan-wrapper:not(.dark) .tiptap pre.mermaid-code-view .mm-keyword {
color: oklch(0.45 0.2 280);
}
.tiptap-plan-wrapper:not(.dark) .tiptap pre.mermaid-code-view .mm-arrow {
color: oklch(0.5 0.15 40);
}
.tiptap-plan-wrapper:not(.dark) .tiptap pre.mermaid-code-view .mm-label {
color: oklch(0.4 0.15 150);
}
.tiptap-plan-wrapper .mermaid-toolbar {
position: absolute;
top: 0.375rem;
right: 0.375rem;
display: flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.375rem;
border-radius: 0.375rem;
border: 1px solid oklch(from var(--border) l c h / 0.5);
background: oklch(from var(--background) l c h / 0.8);
backdrop-filter: blur(4px);
opacity: 0;
transition: opacity 0.15s;
z-index: 10;
}
.tiptap-plan-wrapper .mermaid-container:hover .mermaid-toolbar {
opacity: 1;
}
.tiptap-plan-wrapper .mermaid-zoom-btn {
padding: 0.125rem 0.25rem;
font-size: 0.75rem;
color: var(--muted-foreground);
cursor: pointer;
line-height: 1;
display: flex;
align-items: center;
}
.tiptap-plan-wrapper .mermaid-zoom-btn:hover {
color: var(--foreground);
}
.tiptap-plan-wrapper .mermaid-zoom-label {
font-size: 10px;
color: var(--muted-foreground);
cursor: pointer;
font-variant-numeric: tabular-nums;
min-width: 3ch;
text-align: center;
}
.tiptap-plan-wrapper .mermaid-zoom-label:hover {
color: var(--foreground);
}
.tiptap-plan-wrapper .mermaid-toolbar-sep {
width: 1px;
height: 0.875rem;
background: oklch(from var(--border) l c h / 0.5);
margin: 0 0.125rem;
}
/* Mermaid diagram in markdown renderer (chat messages, PR comments) */
.mermaid-block svg {
max-width: 100%;
height: auto;
}
/* Placeholder for empty plan editor */
.tiptap-plan-wrapper .tiptap p.is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: var(--muted-foreground);
opacity: 0.5;
pointer-events: none;
height: 0;
font-size: 13px;
}
/* Monaco Editor - Diff gutter indicators (colors from --git-addition / --git-deletion CSS vars) */
.monaco-diff-added-gutter {
width: 3px !important;
margin-left: 3px;
background-color: rgb(var(--git-addition)) !important;
}
.monaco-diff-modified-gutter {
width: 3px !important;
margin-left: 3px;
background-color: rgb(var(--git-addition)) !important;
}
.monaco-diff-deleted-gutter {
width: 0 !important;
margin-left: 2px;
border-left: 4px solid rgb(var(--git-deletion)) !important;
}
/* Monaco Editor - Gutter comment hint ("+" hover on decoration lane) */
.monaco-gutter-comment-hint {
cursor: pointer !important;
position: relative !important;
}
.monaco-gutter-comment-hint::after {
content: "+";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
display: flex;
align-items: center;
justify-content: center;
width: 14px;
height: 14px;
font-size: 11px;
font-weight: 700;
line-height: 1;
color: #fff;
border-radius: 3px;
background-color: rgb(99 102 241); /* indigo-500 */
}
/* Monaco Editor - Gutter line selection highlight */
.monaco-gutter-selected-line {
background-color: rgb(139 92 246 / 0.15) !important;
}
.monaco-gutter-selected-deco {
background-color: rgb(139 92 246 / 0.15) !important;
}
/* Monaco Editor - Peek view / references widget overrides */
.monaco-editor .peekview-widget .head {
box-sizing: border-box;
}
.monaco-editor .peekview-widget .head .peekview-title .filename {
font-size: 12px;
font-weight: 500;
}
.monaco-editor .peekview-widget .head .peekview-title .dirname {
font-size: 11px;
}
/* Match references list font size to editor font size */
.monaco-editor .reference-zone-widget .ref-tree .monaco-list-row {
font-size: var(--font-mono-size) !important;
height: 20px !important;
line-height: 20px !important;
}
.monaco-editor .reference-zone-widget .ref-tree .reference-file {
font-size: var(--font-mono-size) !important;
height: 22px !important;
line-height: 22px !important;
}
.monaco-editor .reference-zone-widget .ref-tree .monaco-tl-contents {
padding-left: 4px;
}
.monaco-editor .reference-zone-widget .ref-tree .reference-file .count {
font-size: 10px;
line-height: 14px;
padding: 0 5px;
border-radius: 8px;
}
/* Pipeline node rotating border for running tasks */
@property --border-angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
@keyframes node-border-rotate {
to {
--border-angle: 360deg;
}
}
.node-border-running {
position: relative;
}
.node-border-running::after {
content: "";
position: absolute;
inset: -1px;
border-radius: inherit;
padding: 1.5px;
background: conic-gradient(
from var(--border-angle),
hsl(var(--muted-foreground) / 0.4) 0%,
transparent 12%,
transparent 38%,
hsl(var(--muted-foreground) / 0.4) 50%,
transparent 62%,
transparent 88%,
hsl(var(--muted-foreground) / 0.4) 100%
);
-webkit-mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask:
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask-composite: exclude;
animation: node-border-rotate 3s linear infinite;
pointer-events: none;
}
.chat-message-list {
font-size: 13px;
font-family:
-apple-system, "system-ui", "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji";
}
/* Shiki code block styling */
.shiki-code-block pre.shiki {
margin: 0;
padding: 8px 12px;
white-space: pre-wrap;
overflow-wrap: anywhere;
font-size: var(--font-mono-size);
line-height: 1.5;
background-color: var(--background) !important;
}
.shiki-code-block pre.shiki code {
font-family: var(--font-geist-mono, ui-monospace, monospace);
}
.debug {
border: 1px solid red !important;
}
/* Panel search highlights */
.search-highlight {
background: color-mix(in srgb, var(--color-yellow-300, #fde047) 65%, transparent);
border-radius: 2px;
}
.search-highlight-current {
background: var(--color-orange-400, #fb923c);
outline: 1px solid var(--color-orange-600, #ea580c);
border-radius: 2px;
}
.search-flash {
animation: search-flash 1.2s ease-out;
}
@keyframes search-flash {
0% {
background-color: color-mix(in srgb, var(--color-orange-300, #fdba74) 70%, transparent);
}
100% {
background-color: transparent;
}
}