2026-03-05 20:47:12 +00:00
|
|
|
|
/* Layout */
|
|
|
|
|
|
.app-layout {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
min-height: 100dvh;
|
|
|
|
|
|
background-color: var(--color-bg-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.main-content {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
margin-left: var(--sidebar-width);
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
min-height: 100dvh;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
transition: margin-left var(--duration-normal) var(--ease-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 06:30:49 +00:00
|
|
|
|
.sidebar-is-collapsed .main-content {
|
|
|
|
|
|
margin-left: var(--sidebar-width-collapsed);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.main-content-inner {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-layout-chat {
|
|
|
|
|
|
height: 100vh;
|
|
|
|
|
|
height: 100dvh;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-layout-chat .main-content {
|
|
|
|
|
|
height: 100vh;
|
|
|
|
|
|
height: 100dvh;
|
|
|
|
|
|
min-height: 0;
|
2026-03-09 22:42:47 +00:00
|
|
|
|
min-width: 0;
|
|
|
|
|
|
overflow: hidden;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-layout-chat .main-content-inner {
|
|
|
|
|
|
overflow: hidden;
|
2026-03-09 22:42:47 +00:00
|
|
|
|
min-width: 0;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Footer */
|
|
|
|
|
|
.app-footer {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
background: transparent;
|
|
|
|
|
|
border-top: 1px solid var(--color-border-divider);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
|
|
|
|
margin-top: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-footer-inner {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-footer-version {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-footer-version a {
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
transition: color 150ms;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-footer-version a:hover {
|
2026-03-05 20:47:12 +00:00
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-footer-links {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-footer-links a {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-muted);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
transition: color 150ms;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-footer-links a:hover {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
color: var(--color-text-secondary);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-footer-copyright {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-xs);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-footer-copyright a {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
color: var(--color-text-muted);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
transition: color 150ms;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.app-footer-copyright a:hover {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
color: var(--color-text-secondary);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Mobile header */
|
|
|
|
|
|
.mobile-header {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.hamburger-btn {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
font-size: 1.25rem;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mobile-title {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--color-text-primary);
|
2026-04-27 11:51:29 +00:00
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mobile-header-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mobile-header-btn {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
font-size: 1.05rem;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
min-width: 44px;
|
|
|
|
|
|
min-height: 44px;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
transition: background var(--duration-fast) var(--ease-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
.mobile-header-btn:hover {
|
|
|
|
|
|
background: var(--color-bg-hover);
|
|
|
|
|
|
}
|
|
|
|
|
|
.mobile-header-btn:focus-visible {
|
|
|
|
|
|
outline: 2px solid var(--color-focus-ring);
|
|
|
|
|
|
outline-offset: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mobile-header-avatar {
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.mobile-header-avatar img {
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
.mobile-header-avatar .fa-user-circle {
|
|
|
|
|
|
font-size: 1.5rem;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Sidebar */
|
|
|
|
|
|
.sidebar {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
width: var(--sidebar-width);
|
|
|
|
|
|
height: 100vh;
|
|
|
|
|
|
height: 100dvh;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
background: var(--color-bg-secondary);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border-right: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
z-index: 50;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
box-shadow: var(--shadow-sidebar);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
transition: width var(--duration-normal) var(--ease-spring),
|
|
|
|
|
|
transform var(--duration-normal) var(--ease-spring);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
will-change: transform;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-overlay {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
2026-03-11 06:30:49 +00:00
|
|
|
|
padding: var(--spacing-sm) var(--spacing-sm);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border-bottom: 1px solid var(--color-border-subtle);
|
2026-03-11 06:30:49 +00:00
|
|
|
|
min-height: 44px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-logo-link {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-logo-img {
|
|
|
|
|
|
width: 100%;
|
2026-03-11 06:30:49 +00:00
|
|
|
|
max-width: 120px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
height: auto;
|
|
|
|
|
|
padding: 0 var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 06:30:49 +00:00
|
|
|
|
.sidebar-logo-icon {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-logo-icon-img {
|
|
|
|
|
|
width: 28px;
|
|
|
|
|
|
height: 28px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.sidebar-close-btn {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
font-size: 1.25rem;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-nav {
|
|
|
|
|
|
flex: 1;
|
2026-03-11 06:30:49 +00:00
|
|
|
|
padding: 2px 0;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-section {
|
2026-03-11 06:30:49 +00:00
|
|
|
|
padding: 2px 0;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-section-title {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-xs);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
text-transform: uppercase;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
2026-03-11 06:30:49 +00:00
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 01:08:02 +00:00
|
|
|
|
.sidebar-section-toggle {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
transition: color var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-section-toggle:hover {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
color: var(--color-text-primary);
|
2026-03-21 01:08:02 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-section-chevron {
|
|
|
|
|
|
font-size: 0.5rem;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
opacity: 0.6;
|
|
|
|
|
|
transition: transform var(--duration-fast), opacity var(--duration-fast);
|
2026-03-21 01:08:02 +00:00
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 21:53:03 +00:00
|
|
|
|
.sidebar-section-toggle:hover .sidebar-section-chevron {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 01:08:02 +00:00
|
|
|
|
.sidebar-section-toggle.open .sidebar-section-chevron {
|
|
|
|
|
|
transform: rotate(90deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.nav-item {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
position: relative;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
padding: 8px var(--spacing-md) 8px calc(var(--spacing-sm) + 2px);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
text-decoration: none;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
font-weight: var(--font-weight-medium);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
transition: color var(--duration-normal) var(--ease-spring),
|
|
|
|
|
|
background var(--duration-normal) var(--ease-spring),
|
|
|
|
|
|
box-shadow var(--duration-normal) var(--ease-spring);
|
2026-03-11 06:30:49 +00:00
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 21:53:03 +00:00
|
|
|
|
.nav-item:hover:not(.active) {
|
2026-03-05 20:47:12 +00:00
|
|
|
|
color: var(--color-text-primary);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
background: var(--color-surface-elevated);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-item.active {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
background: var(--color-primary-light);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
box-shadow: inset 2px 0 0 var(--color-primary);
|
|
|
|
|
|
font-weight: var(--font-weight-medium);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-icon {
|
2026-03-11 06:30:49 +00:00
|
|
|
|
width: 18px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
text-align: center;
|
2026-03-11 06:30:49 +00:00
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
font-size: 0.85rem;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-label {
|
|
|
|
|
|
flex: 1;
|
2026-03-11 06:30:49 +00:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
2026-03-19 20:40:51 +00:00
|
|
|
|
transition: opacity 150ms ease;
|
2026-03-11 06:30:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.nav-external {
|
|
|
|
|
|
font-size: 0.55rem;
|
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
|
flex-shrink: 0;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-footer {
|
2026-03-11 06:30:49 +00:00
|
|
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border-top: 1px solid var(--color-border-subtle);
|
2026-03-11 06:30:49 +00:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-user {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: var(--spacing-xs) 0;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-user-avatar {
|
|
|
|
|
|
width: 20px;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-user-avatar-icon {
|
|
|
|
|
|
font-size: 1.25rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-user-link {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
padding: 2px var(--spacing-xs);
|
|
|
|
|
|
margin: -2px calc(-1 * var(--spacing-xs));
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
color: inherit;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background var(--duration-fast), color var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-user-link:hover {
|
|
|
|
|
|
background: var(--color-bg-hover);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-user-name {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-logout-btn {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 2px 4px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
transition: color var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-logout-btn:hover {
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar.collapsed .sidebar-user {
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar.collapsed .sidebar-user-link {
|
|
|
|
|
|
flex: 0;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar.collapsed .sidebar-user-name,
|
|
|
|
|
|
.sidebar.collapsed .sidebar-logout-btn {
|
|
|
|
|
|
display: none;
|
2026-03-11 06:30:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-collapse-btn {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
2026-03-11 06:30:49 +00:00
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
transition: color var(--duration-fast), background var(--duration-fast);
|
2026-03-11 06:30:49 +00:00
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-collapse-btn:hover {
|
|
|
|
|
|
color: var(--color-text-primary);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
background: var(--color-surface-hover);
|
2026-03-11 06:30:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Collapsed sidebar (desktop only) */
|
|
|
|
|
|
.sidebar.collapsed {
|
|
|
|
|
|
width: var(--sidebar-width-collapsed);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar.collapsed .sidebar-logo-link {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar.collapsed .sidebar-logo-icon {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar.collapsed .sidebar-header {
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar.collapsed .nav-label,
|
|
|
|
|
|
.sidebar.collapsed .nav-external,
|
|
|
|
|
|
.sidebar.collapsed .sidebar-section-title {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 01:08:02 +00:00
|
|
|
|
.sidebar.collapsed .sidebar-section-chevron {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-11 06:30:49 +00:00
|
|
|
|
.sidebar.collapsed .nav-item {
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
|
border-left-width: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar.collapsed .nav-icon {
|
|
|
|
|
|
width: auto;
|
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar.collapsed .sidebar-footer {
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar.collapsed .theme-toggle {
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar.collapsed .theme-toggle .nav-label {
|
|
|
|
|
|
display: none;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Theme toggle */
|
|
|
|
|
|
.theme-toggle {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
transition: all var(--duration-fast) var(--ease-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
.theme-toggle:hover {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
border-color: var(--color-primary-border);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(react-ui): add multilingual (i18n) support (#9642)
Adds end-to-end internationalization to the React UI with five seed
languages (English, Italian, Spanish, German, Simplified Chinese) and
a sidebar-footer language switcher next to the existing theme toggle.
Library: react-i18next + i18next + i18next-http-backend +
i18next-browser-languagedetector. The detector caches the user's
choice in localStorage (key `localai-language`, mirroring the existing
`localai-theme` convention) and updates the `<html lang>` attribute on
change. fallbackLng is `en`, so any missing translation in another
locale falls back transparently.
Translation files live under `public/locales/<lng>/<ns>.json`. They
ride along with the existing `//go:embed react-ui/dist/*` directive,
but the previous SPA route in core/http/app.go only exposed
`/assets/*` from the embedded React build. This commit generalizes
the asset handler into a `serveReactSubdir(subdir)` helper and adds a
matching `/locales/*` route so i18next-http-backend can fetch the
JSONs at runtime. The http-backend `loadPath` is built via the
existing `apiUrl()` helper so instances served under a sub-path (e.g.
`<base href="/ui/">`) resolve correctly.
Namespaces (13): common, nav, errors, auth, home, models, importModel,
chat, agents, skills, collections, media, admin. Translated UI surfaces
include the sidebar/header/footer chrome, login + account flows, the
Home dashboard (incl. the manage-by-chat assistant CTA), the model
gallery + import flow, the chat experience (Chat.jsx + ChatsMenu),
agents/skills/collections list pages, the studio media tabs (Image,
Video, TTS), and the admin page-headers (Settings incl. its section
nav, Manage, Backends, Traces, Nodes, P2P, Users, Usage). Shared
components (ConfirmDialog, Toast) take their default labels from the
common namespace so callers don't need to pass strings explicitly.
Tooling for incremental adoption is included:
- `i18next-parser.config.js` + `npm run i18n:extract` to sweep `t()`
keys into the JSON skeletons.
- `scripts/translate-locales.mjs` (one-off helper) to bootstrap
non-English locales from English source via OpenAI or Anthropic
APIs, with --copy mode as a placeholder fallback. Idempotent;
preserves existing translations unless --overwrite is passed.
Larger config-driven pages (ModelEditor, Settings deep field forms,
AgentChat/AgentCreate, SkillEdit, CollectionDetails, Talk, Sound,
biometrics, FineTune/Quantize, Users modals, Nodes/P2P install
pickers, BackendLogs, Traces deep filters, Explorer) intentionally
keep their inner content untranslated for now — they fall back to
English via fallbackLng so functionality is unaffected, and the
extracted-strings pattern + the bootstrap script make follow-up
extraction straightforward.
The initial Suspense fallback at the root in main.jsx covers the
first JSON fetch on cold load. A simple `.app-boot-spinner` styled
in App.css provides a non-empty paint while the first namespace
loads.
Assisted-by: Claude:claude-opus-4-7 [Bash Read Edit Write Agent]
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-05-02 20:42:08 +00:00
|
|
|
|
/* Language switcher */
|
|
|
|
|
|
.language-switcher {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
}
|
|
|
|
|
|
.language-switcher-trigger {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.language-switcher-code {
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
letter-spacing: 0.04em;
|
|
|
|
|
|
}
|
|
|
|
|
|
.sidebar.collapsed .language-switcher-code {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.language-switcher-menu {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: calc(100% + 8px);
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
min-width: 160px;
|
|
|
|
|
|
background: var(--color-bg-elevated, var(--color-bg-secondary));
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
box-shadow: var(--shadow-lg, 0 8px 24px rgba(0, 0, 0, 0.18));
|
|
|
|
|
|
list-style: none;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
z-index: 1000;
|
|
|
|
|
|
}
|
|
|
|
|
|
.sidebar.collapsed .language-switcher-menu {
|
|
|
|
|
|
left: calc(100% + 8px);
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.language-switcher-menu li {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.language-switcher-option {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
}
|
|
|
|
|
|
.language-switcher-option:hover {
|
|
|
|
|
|
background: var(--color-bg-tertiary, rgba(255, 255, 255, 0.04));
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.language-switcher-option.active {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.language-switcher-flag {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
width: 22px;
|
|
|
|
|
|
letter-spacing: 0.04em;
|
|
|
|
|
|
}
|
|
|
|
|
|
.language-switcher-name {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
.language-switcher-check {
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* App boot fallback (rendered while initial i18n namespaces load) */
|
|
|
|
|
|
.app-boot-spinner {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
background: var(--color-bg-primary, #111);
|
|
|
|
|
|
}
|
|
|
|
|
|
.app-boot-spinner-dot {
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
border: 3px solid var(--color-border-subtle, rgba(255, 255, 255, 0.15));
|
|
|
|
|
|
border-top-color: var(--color-primary, #4f8cff);
|
|
|
|
|
|
animation: app-boot-spin 0.8s linear infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
@keyframes app-boot-spin {
|
|
|
|
|
|
to { transform: rotate(360deg); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
/* Operations bar */
|
|
|
|
|
|
.operations-bar {
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
.operation-text {
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
}
|
|
|
|
|
|
.operation-progress {
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.operation-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
gap: var(--spacing-md);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
padding: var(--spacing-xs) 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.operation-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
flex: 2 1 0;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.operation-info > .operation-text {
|
|
|
|
|
|
flex: 1 1 auto;
|
2026-03-12 17:19:06 +00:00
|
|
|
|
min-width: 0;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.operation-spinner {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
width: 16px;
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
box-sizing: border-box;
|
|
|
|
|
|
border: 2px solid var(--color-border-default);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border-top-color: var(--color-primary);
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
animation: spin 0.8s linear infinite;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
display: inline-block;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.operation-text {
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
2026-03-12 17:19:06 +00:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.operation-progress {
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.operation-bar-container {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
flex: 0 1 160px;
|
|
|
|
|
|
min-width: 80px;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
height: 3px;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
border-radius: var(--radius-full);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.operation-bar {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: var(--color-primary);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
border-radius: var(--radius-full);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
transition: width var(--duration-slow) var(--ease-spring);
|
|
|
|
|
|
animation: opsBarBreathe 1.4s ease-in-out infinite;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Inline install indicator — used in table rows (Models, Backends) */
|
|
|
|
|
|
.inline-install {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.inline-install__row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.inline-install__label {
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
font-weight: var(--font-weight-medium);
|
|
|
|
|
|
white-space: nowrap;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.operation-cancel {
|
2026-03-12 17:19:06 +00:00
|
|
|
|
flex-shrink: 0;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
2026-03-12 17:19:06 +00:00
|
|
|
|
padding: 4px 6px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.operation-cancel:hover {
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Toast */
|
|
|
|
|
|
.toast-container {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: var(--spacing-lg);
|
|
|
|
|
|
right: var(--spacing-lg);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
z-index: 1100;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toast {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border-radius: var(--radius-lg);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
box-shadow: var(--shadow-sm);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
letter-spacing: -0.005em;
|
|
|
|
|
|
animation: toastSlideIn var(--duration-normal) var(--ease-spring);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
min-width: 280px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toast-enter {
|
|
|
|
|
|
opacity: 0;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
transform: translateX(12px);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 20:40:51 +00:00
|
|
|
|
.toast-exit {
|
|
|
|
|
|
opacity: 0;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
transform: translateX(12px);
|
|
|
|
|
|
transition: opacity var(--duration-fast) var(--ease-spring),
|
|
|
|
|
|
transform var(--duration-fast) var(--ease-spring);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.toast-success {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
box-shadow: inset 3px 0 0 var(--color-success), var(--shadow-sm);
|
|
|
|
|
|
border-color: var(--color-border-subtle);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
.toast-error {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
box-shadow: inset 3px 0 0 var(--color-error), var(--shadow-sm);
|
|
|
|
|
|
border-color: var(--color-border-subtle);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
.toast-warning {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
box-shadow: inset 3px 0 0 var(--color-warning), var(--shadow-sm);
|
|
|
|
|
|
border-color: var(--color-border-subtle);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
.toast-info {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
box-shadow: inset 3px 0 0 var(--color-info), var(--shadow-sm);
|
|
|
|
|
|
border-color: var(--color-border-subtle);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toast-close {
|
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: inherit;
|
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.toast-close:hover { opacity: 1; }
|
2026-03-20 14:06:07 +00:00
|
|
|
|
.toast-link {
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: inherit;
|
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
margin-left: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.toast-link:hover { opacity: 1; }
|
|
|
|
|
|
|
|
|
|
|
|
/* Chat error trace link */
|
|
|
|
|
|
.chat-error-trace-link {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
text-decoration: none;
|
|
|
|
|
|
margin-top: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-error-trace-link:hover {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
}
|
2026-03-05 20:47:12 +00:00
|
|
|
|
|
|
|
|
|
|
/* Spinner */
|
|
|
|
|
|
.spinner {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.spinner-ring {
|
|
|
|
|
|
border: 3px solid var(--color-border-subtle);
|
|
|
|
|
|
border-top-color: var(--color-primary);
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
animation: spin 0.8s linear infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
.spinner-sm .spinner-ring { width: 16px; height: 16px; }
|
|
|
|
|
|
.spinner-md .spinner-ring { width: 24px; height: 24px; }
|
|
|
|
|
|
.spinner-lg .spinner-ring { width: 40px; height: 40px; }
|
|
|
|
|
|
|
|
|
|
|
|
/* Model selector */
|
|
|
|
|
|
.model-selector {
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
border: 1px solid var(--color-border-default);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: border-color var(--duration-fast);
|
|
|
|
|
|
min-width: 180px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.model-selector:focus {
|
|
|
|
|
|
border-color: var(--color-border-strong);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Resource monitor */
|
|
|
|
|
|
.resource-monitor {
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.resource-monitor-title {
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
margin-bottom: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.resource-gpu-list {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.resource-gpu-card {
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
padding: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.resource-gpu-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
margin-bottom: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.resource-gpu-name {
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.resource-gpu-vendor {
|
|
|
|
|
|
font-size: 0.6875rem;
|
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
|
background: var(--color-accent-light);
|
|
|
|
|
|
color: var(--color-accent);
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.resource-gpu-stats {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
margin-top: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.resource-bar-container {
|
|
|
|
|
|
height: 4px;
|
|
|
|
|
|
background: var(--color-bg-primary);
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.resource-bar {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: var(--color-primary);
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
transition: width 500ms ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.resource-bar-ram {
|
|
|
|
|
|
background: var(--color-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.resource-no-gpu {
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
padding: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.resource-ram {
|
|
|
|
|
|
margin-top: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.resource-ram-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
margin-bottom: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.resource-monitor-compact {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.resource-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 11:51:29 +00:00
|
|
|
|
/* Common page styles — width archetype is opt-in via modifiers.
|
|
|
|
|
|
Default cap fits 9-column data tables on ultrawide displays without
|
|
|
|
|
|
feeling untethered. Add .page--narrow for forms / single-record edit
|
|
|
|
|
|
views, .page--wide for full-bleed (chat shells, log streams). */
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.page {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
padding: var(--spacing-xl) var(--spacing-xl) var(--spacing-2xl);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
width: 100%;
|
2026-04-27 11:51:29 +00:00
|
|
|
|
max-width: var(--page-max, var(--page-max-default));
|
|
|
|
|
|
margin: 0 auto;
|
2026-03-19 20:40:51 +00:00
|
|
|
|
animation: fadeIn var(--duration-normal) var(--ease-default);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 11:51:29 +00:00
|
|
|
|
.page--narrow { --page-max: var(--page-max-narrow); }
|
|
|
|
|
|
.page--medium { --page-max: var(--page-max-medium); }
|
|
|
|
|
|
.page--wide { --page-max: var(--page-max-wide); }
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.page-header {
|
|
|
|
|
|
margin-bottom: var(--spacing-xl);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-title {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-2xl);
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
letter-spacing: -0.015em;
|
|
|
|
|
|
line-height: var(--leading-tight);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
margin-bottom: var(--spacing-xs);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
color: var(--color-text-primary);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.page-subtitle {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-sm);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
color: var(--color-text-secondary);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
line-height: var(--leading-normal);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Cards */
|
|
|
|
|
|
.card {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
background: var(--color-bg-secondary);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
padding: var(--spacing-lg);
|
|
|
|
|
|
box-shadow: var(--shadow-subtle), var(--shadow-inset-top);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
transition: border-color var(--duration-normal) var(--ease-spring),
|
|
|
|
|
|
box-shadow var(--duration-normal) var(--ease-spring),
|
|
|
|
|
|
transform var(--duration-normal) var(--ease-spring);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.card:hover {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
border-color: var(--color-border-strong);
|
|
|
|
|
|
box-shadow: var(--shadow-sm), var(--shadow-inset-top);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
transform: translateY(-1px);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
/* Accent-rail variant — editorial left bar for highlighted cards */
|
|
|
|
|
|
.card--accent {
|
|
|
|
|
|
box-shadow: inset 2px 0 0 var(--color-primary), var(--shadow-subtle), var(--shadow-inset-top);
|
|
|
|
|
|
}
|
|
|
|
|
|
.card--accent:hover {
|
|
|
|
|
|
box-shadow: inset 2px 0 0 var(--color-primary), var(--shadow-sm), var(--shadow-inset-top);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.card-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
2026-04-14 21:53:03 +00:00
|
|
|
|
gap: var(--spacing-lg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Form rows — consistent label+control rhythm */
|
|
|
|
|
|
.form-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
padding: var(--spacing-md) 0;
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-divider);
|
|
|
|
|
|
}
|
|
|
|
|
|
.form-row:last-child {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.form-row__label {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
.form-row__label-text {
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
font-weight: var(--font-weight-medium);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.form-row__hint {
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
line-height: var(--leading-snug);
|
|
|
|
|
|
}
|
|
|
|
|
|
.form-row__control {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-group__title {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.06em;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
padding: var(--spacing-md) 0 var(--spacing-sm);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-divider);
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.form-group__title i {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.form-group__body {
|
|
|
|
|
|
padding-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.form-group__body:last-child {
|
|
|
|
|
|
padding-bottom: var(--spacing-lg);
|
|
|
|
|
|
}
|
|
|
|
|
|
.form-group__actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: var(--spacing-md) 0 var(--spacing-lg);
|
|
|
|
|
|
border-top: 1px solid var(--color-border-divider);
|
|
|
|
|
|
margin-top: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Form layout grids */
|
|
|
|
|
|
.form-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.form-grid-2col {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.form-grid-3col {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (max-width: 720px) {
|
|
|
|
|
|
.form-grid-2col, .form-grid-3col {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-field {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.form-field__label {
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: var(--font-weight-medium);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.04em;
|
|
|
|
|
|
}
|
|
|
|
|
|
.form-field__hint {
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
line-height: var(--leading-snug);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Button modifiers */
|
|
|
|
|
|
.btn-full {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Progress bar */
|
|
|
|
|
|
.progress-bar {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
}
|
|
|
|
|
|
.progress-bar__fill {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: var(--color-primary);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
color: var(--color-primary-text);
|
|
|
|
|
|
transition: width 300ms var(--ease-default);
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
padding: 0 var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.progress-bar__fill--error {
|
|
|
|
|
|
background: var(--color-error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Log tail viewport */
|
|
|
|
|
|
.log-tail {
|
|
|
|
|
|
max-height: 180px;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
font-family: var(--font-mono);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
line-height: var(--leading-snug);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.log-tail__line {
|
|
|
|
|
|
padding: 1px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.log-tail__line--error {
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Result quote (TTS / Sound prompt echo) */
|
|
|
|
|
|
.result-quote {
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
font-style: italic;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
line-height: var(--leading-normal);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Data table — used by Quantize jobs list, Traces, etc. */
|
|
|
|
|
|
.data-table {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.data-table th {
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.04em;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-default);
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
}
|
|
|
|
|
|
.data-table td {
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-divider);
|
|
|
|
|
|
vertical-align: middle;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.data-table tbody tr {
|
|
|
|
|
|
transition: background var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.data-table tbody tr:hover {
|
|
|
|
|
|
background: var(--color-surface-hover);
|
|
|
|
|
|
}
|
|
|
|
|
|
.data-table tbody tr.is-selected {
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
.data-table tbody tr:last-child td {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.data-table__actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
.data-table__truncate {
|
|
|
|
|
|
max-width: 280px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Quantize page */
|
|
|
|
|
|
.quantize-page__header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-form {
|
|
|
|
|
|
margin-bottom: var(--spacing-lg);
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-form__quant-row {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr 1fr;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-form__quant-row > :only-child {
|
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (max-width: 520px) {
|
|
|
|
|
|
.quantize-form__quant-row {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-progress-card {
|
|
|
|
|
|
margin-bottom: var(--spacing-lg);
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-progress-card__header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-progress-card__title {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: var(--text-base);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-progress-card__title i {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-progress-card__status {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-progress-card__message {
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-progress-card .progress-bar {
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-import-card {
|
|
|
|
|
|
margin-bottom: var(--spacing-lg);
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-import-card__title {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
margin: 0 0 var(--spacing-md);
|
|
|
|
|
|
font-size: var(--text-base);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-import-card__title i {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-import-card__row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-import-card__name {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 220px;
|
|
|
|
|
|
max-width: 320px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-jobs {
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-jobs__title {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-divider);
|
|
|
|
|
|
font-size: var(--text-base);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-jobs__title i {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.quantize-jobs__scroll {
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Segmented control (Sound mode toggle etc.) */
|
|
|
|
|
|
.segmented {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
padding: 3px;
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.segmented__item {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
padding: 6px var(--spacing-md);
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: var(--font-weight-medium);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background var(--duration-fast), color var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.segmented__item:hover {
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.segmented__item.is-active {
|
|
|
|
|
|
background: var(--color-surface-raised);
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
box-shadow: var(--shadow-subtle);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Inline checkbox row */
|
|
|
|
|
|
.checkbox-row {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: 10px var(--spacing-md);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
transition: border-color var(--duration-fast);
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.checkbox-row:hover {
|
|
|
|
|
|
border-color: var(--color-border-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
.checkbox-row input[type="checkbox"] {
|
|
|
|
|
|
accent-color: var(--color-primary);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Audio result wrapper (TTS/Sound) */
|
|
|
|
|
|
.audio-result {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: stretch;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
gap: var(--spacing-md);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 480px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-result__player {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-result__actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Media empty state */
|
|
|
|
|
|
.media-empty {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
padding: var(--spacing-xl) var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.media-empty__icon {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: 2.75rem;
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
opacity: 0.35;
|
|
|
|
|
|
}
|
|
|
|
|
|
.media-empty p {
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
margin: 0;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Buttons */
|
|
|
|
|
|
.btn {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
justify-content: center;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
gap: var(--spacing-xs);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
padding: 0.5rem var(--spacing-md);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
min-height: 34px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border-radius: var(--radius-md);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-sm);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
font-family: inherit;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-weight: var(--font-weight-medium);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
letter-spacing: -0.005em;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
cursor: pointer;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
border: 1px solid transparent;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
transition: background var(--duration-normal) var(--ease-spring),
|
|
|
|
|
|
color var(--duration-normal) var(--ease-spring),
|
|
|
|
|
|
border-color var(--duration-normal) var(--ease-spring),
|
|
|
|
|
|
box-shadow var(--duration-normal) var(--ease-spring),
|
|
|
|
|
|
filter var(--duration-normal) var(--ease-spring),
|
|
|
|
|
|
transform var(--duration-normal) var(--ease-spring);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
text-decoration: none;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn:focus-visible {
|
|
|
|
|
|
outline: none;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
box-shadow: 0 0 0 3px var(--color-focus-ring);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Global focus ring — any interactive that isn't a .btn */
|
|
|
|
|
|
:where(a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])):focus-visible {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
outline: none;
|
|
|
|
|
|
box-shadow: 0 0 0 3px var(--color-focus-ring);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
border-radius: var(--radius-sm);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-primary {
|
|
|
|
|
|
background: var(--color-primary);
|
|
|
|
|
|
color: var(--color-primary-text);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.25), var(--shadow-inset-hi);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-04-14 21:53:03 +00:00
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
filter: brightness(1.06);
|
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3), var(--shadow-inset-hi);
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn-primary:focus-visible:not(:disabled) {
|
|
|
|
|
|
box-shadow: 0 0 0 3px var(--color-focus-ring), var(--shadow-inset-hi);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-secondary {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
background: var(--color-surface-elevated);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
color: var(--color-text-primary);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
border-color: var(--color-border-default);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-04-14 21:53:03 +00:00
|
|
|
|
.btn-secondary:hover:not(:disabled) {
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border-color: var(--color-border-strong);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
background: var(--color-surface-hover);
|
|
|
|
|
|
transform: translateY(-1px);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-ghost {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
border-color: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn-ghost:hover:not(:disabled) {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
background: var(--color-surface-elevated);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
color: var(--color-text-primary);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-danger {
|
|
|
|
|
|
background: var(--color-error-light);
|
|
|
|
|
|
color: var(--color-error);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
border-color: var(--color-error-border);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-04-14 21:53:03 +00:00
|
|
|
|
.btn-danger:hover:not(:disabled) {
|
|
|
|
|
|
background: var(--color-error);
|
|
|
|
|
|
color: var(--color-text-inverse);
|
|
|
|
|
|
border-color: var(--color-error);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
filter: brightness(1.04);
|
|
|
|
|
|
transform: translateY(-1px);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.btn-sm {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
padding: 0.35rem var(--spacing-sm);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
min-height: 28px;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-xs);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
letter-spacing: 0;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 20:40:51 +00:00
|
|
|
|
.btn:active:not(:disabled) {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
filter: brightness(0.95);
|
|
|
|
|
|
transform: translateY(0);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.btn:disabled {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
opacity: 0.45;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
cursor: not-allowed;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
filter: none;
|
|
|
|
|
|
transform: none;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 21:53:03 +00:00
|
|
|
|
/* Toggle switch */
|
|
|
|
|
|
.toggle {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
width: 38px;
|
|
|
|
|
|
height: 22px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.toggle input {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
width: 0;
|
|
|
|
|
|
height: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.toggle__track {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
background: var(--color-toggle-off);
|
|
|
|
|
|
transition: background var(--duration-normal) var(--ease-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
.toggle__thumb {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 2px;
|
|
|
|
|
|
left: 2px;
|
|
|
|
|
|
width: 18px;
|
|
|
|
|
|
height: 18px;
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
background: #ffffff;
|
|
|
|
|
|
box-shadow: var(--shadow-sm);
|
|
|
|
|
|
transition: transform var(--duration-normal) var(--ease-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
.toggle--on .toggle__track {
|
|
|
|
|
|
background: var(--color-toggle-on);
|
|
|
|
|
|
}
|
|
|
|
|
|
.toggle--on .toggle__thumb {
|
|
|
|
|
|
transform: translateX(16px);
|
|
|
|
|
|
}
|
|
|
|
|
|
.toggle:hover:not(.toggle--disabled) .toggle__track {
|
|
|
|
|
|
filter: brightness(1.08);
|
|
|
|
|
|
}
|
|
|
|
|
|
.toggle:focus-within .toggle__track {
|
|
|
|
|
|
box-shadow: 0 0 0 3px var(--color-border-focus);
|
|
|
|
|
|
}
|
|
|
|
|
|
.toggle--disabled {
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
opacity: 0.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
/* Inputs — sunken well, quiet border, sage focus ring */
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.input {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
background: var(--color-surface-sunken);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
border: 1px solid var(--color-border-default);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
font-size: var(--text-sm);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
font-family: inherit;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
letter-spacing: -0.005em;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
outline: none;
|
|
|
|
|
|
width: 100%;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
transition: border-color var(--duration-normal) var(--ease-spring),
|
|
|
|
|
|
box-shadow var(--duration-normal) var(--ease-spring),
|
|
|
|
|
|
background var(--duration-normal) var(--ease-spring);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-04-14 21:53:03 +00:00
|
|
|
|
.input::placeholder {
|
|
|
|
|
|
color: var(--color-text-muted);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
opacity: 0.8;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
.input:hover:not(:disabled):not(:focus) {
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border-color: var(--color-border-strong);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
.input:focus {
|
|
|
|
|
|
border-color: var(--color-primary);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
box-shadow: 0 0 0 3px var(--color-focus-ring);
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
.input:disabled {
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: not-allowed;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
opacity: 0.7;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
select.input {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding-right: var(--spacing-xl);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
.input-mono {
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
letter-spacing: -0.01em;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.textarea {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
background: var(--color-surface-sunken);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
border: 1px solid var(--color-border-default);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
font-size: var(--text-sm);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
font-family: inherit;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
letter-spacing: -0.005em;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
outline: none;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
resize: vertical;
|
|
|
|
|
|
min-height: 80px;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
line-height: var(--leading-normal);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
transition: border-color var(--duration-normal) var(--ease-spring),
|
|
|
|
|
|
box-shadow var(--duration-normal) var(--ease-spring);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-04-14 21:53:03 +00:00
|
|
|
|
.textarea::placeholder {
|
|
|
|
|
|
color: var(--color-text-muted);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
opacity: 0.8;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
.textarea:hover:not(:disabled):not(:focus) {
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border-color: var(--color-border-strong);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
.textarea:focus {
|
|
|
|
|
|
border-color: var(--color-primary);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
box-shadow: 0 0 0 3px var(--color-focus-ring);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
.textarea:disabled {
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: not-allowed;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
opacity: 0.7;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 12:42:23 +00:00
|
|
|
|
/* CodeMirror editor wrapper */
|
|
|
|
|
|
.code-editor-cm .cm-editor {
|
|
|
|
|
|
border: 1px solid var(--color-border-default);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-04-07 12:42:23 +00:00
|
|
|
|
.code-editor-cm .cm-editor.cm-focused {
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border-color: var(--color-border-strong);
|
2026-04-07 12:42:23 +00:00
|
|
|
|
outline: none;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Form groups */
|
|
|
|
|
|
.form-group {
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-label {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
margin-bottom: var(--spacing-xs);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
/* Badges — sharp editorial rectangles, mono caps */
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.badge {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
padding: 2px 8px;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
font-size: 0.625rem;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
font-weight: 500;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.badge-success {
|
|
|
|
|
|
background: var(--color-success-light);
|
|
|
|
|
|
color: var(--color-success);
|
|
|
|
|
|
}
|
|
|
|
|
|
.badge-error {
|
|
|
|
|
|
background: var(--color-error-light);
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
}
|
|
|
|
|
|
.badge-info {
|
|
|
|
|
|
background: var(--color-info-light);
|
|
|
|
|
|
color: var(--color-info);
|
|
|
|
|
|
}
|
|
|
|
|
|
.badge-warning {
|
|
|
|
|
|
background: var(--color-warning-light);
|
|
|
|
|
|
color: var(--color-warning);
|
|
|
|
|
|
}
|
feat(distributed): sync state with frontends, better backend management reporting (#9426)
* fix(distributed): detect backend upgrades across worker nodes
Before this change `DistributedBackendManager.CheckUpgrades` delegated to the
local manager, which read backends from the frontend filesystem. In
distributed deployments the frontend has no backends installed locally —
they live on workers — so the upgrade-detection loop never ran and the UI
silently never surfaced upgrades even when the gallery advertised newer
versions or digests.
Worker-side: NATS backend.list reply now carries Version, URI and Digest
for each installed backend (read from metadata.json).
Frontend-side: DistributedBackendManager.ListBackends aggregates per-node
refs (name, status, version, digest) instead of deduping, and CheckUpgrades
feeds that aggregation into gallery.CheckUpgradesAgainst — a new entrypoint
factored out of CheckBackendUpgrades so both paths share the same core
logic.
Cluster drift policy: when per-node version/digest tuples disagree, the
backend is flagged upgradeable regardless of whether any single node
matches the gallery, and UpgradeInfo.NodeDrift enumerates the outliers so
operators can see *why* it is out of sync. The next upgrade-all realigns
the cluster.
Tests cover: drift detection, unanimous-match (no upgrade), and the
empty-installed-version path that the old distributed code silently
missed.
* feat(ui): surface backend upgrades in the System page
The System page (Manage.jsx) only showed updates as a tiny inline arrow,
so operators routinely missed them. Port the Backend Gallery's upgrade UX
so System speaks the same visual language:
- Yellow banner at the top of the Backends tab when upgrades are pending,
with an "Upgrade all" button (serial fan-out, matches the gallery) and a
"Updates only" filter toggle.
- Warning pill (↑ N) next to the tab label so the count is glanceable even
when the banner is scrolled out of view.
- Per-row labeled "Upgrade to vX.Y" button (replaces the icon-only button
that silently flipped semantics between Reinstall and Upgrade), plus an
"Update available" badge in the new Version column.
- New columns: Version (with upgrade + drift chips), Nodes (per-node
attribution badges for distributed mode, degrading to a compact
"on N nodes · M offline" chip above three nodes), Installed (relative
time).
- System backends render a "Protected" chip instead of a bare "—" so rows
still align and the reason is obvious.
- Delete uses the softer btn-danger-ghost so rows don't scream red; the
ConfirmDialog still owns the "are you sure".
The upgrade checker also needed the same per-worker fix as the previous
commit: NewUpgradeChecker now takes a BackendManager getter so its
periodic runs call the distributed CheckUpgrades (which asks workers)
instead of the empty frontend filesystem. Without this the /api/backends/
upgrades endpoint stayed empty in distributed mode even with the protocol
change in place.
New CSS primitives — .upgrade-banner, .tab-pill, .badge-row, .cell-stack,
.cell-mono, .cell-muted, .row-actions, .btn-danger-ghost — all live in
App.css so other pages can adopt them without duplicating styles.
* feat(ui): polish the Nodes page so it reads like a product
The Nodes page was the biggest visual liability in distributed mode.
Rework the main dashboard surfaces in place without changing behavior:
StatCards: uniform height (96px min), left accent bar colored by the
metric's semantic (success/warning/error/primary), icon lives in a
36x36 soft-tinted chip top-right, value is left-aligned and large.
Grid auto-fills so the row doesn't collapse on narrow viewports. This
replaces the previous thin-bordered boxes with inconsistent heights.
Table rows: expandable rows now show a chevron cue on the left (rotates
on expand) so users know rows open. Status cell became a dedicated chip
with an LED-style halo dot instead of a bare bullet. Action buttons gained
labels — "Approve", "Resume", "Drain" — so the icons aren't doing all
the semantic work; the destructive remove action uses the softer
btn-danger-ghost variant so rows don't scream red, with the ConfirmDialog
still owning the real "are you sure". Applied cell-mono/cell-muted
utility classes so label chips and addresses share one spacing/font
grammar instead of re-declaring inline styles everywhere.
Expanded drawer: empty states for Loaded Models and Installed Backends
now render as a proper drawer-empty card (dashed border, icon, one-line
hint) instead of a plain muted string that read like broken formatting.
Tabs: three inline-styled buttons became the shared .tab class so they
inherit focus ring, hover state, and the rest of the design system —
matches the System page.
"Add more workers" toggle turned into a .nodes-add-worker dashed-border
button labelled "Register a new worker" (action voice) instead of a
chevron + muted link that operators kept mistaking for broken text.
New shared CSS primitives carry over to other pages:
.stat-grid + .stat-card, .row-chevron, .node-status, .drawer-empty,
.nodes-add-worker.
* feat(distributed): durable backend fan-out + state reconciliation
Two connected problems handled together:
1) Backend delete/install/upgrade used to silently skip non-healthy nodes,
so a delete during an outage left a zombie on the offline node once it
returned. The fan-out now records intent in a new pending_backend_ops
table before attempting the NATS round-trip. Currently-healthy nodes
get an immediate attempt; everyone else is queued. Unique index on
(node_id, backend, op) means reissuing the same operation refreshes
next_retry_at instead of stacking duplicates.
2) Loaded-model state could drift from reality: a worker OOM'd, got
killed, or restarted a backend process would leave a node_models row
claiming the model was still loaded, feeding ghost entries into the
/api/nodes/models listing and the router's scheduling decisions.
The existing ReplicaReconciler gains two new passes that run under a
fresh KeyStateReconciler advisory lock (non-blocking, so one wedged
frontend doesn't freeze the cluster):
- drainPendingBackendOps: retries queued ops whose next_retry_at has
passed on currently-healthy nodes. Success deletes the row; failure
bumps attempts and pushes next_retry_at out with exponential backoff
(30s → 15m cap). ErrNoResponders also marks the node unhealthy.
- probeLoadedModels: gRPC-HealthChecks addresses the DB thinks are
loaded but hasn't seen touched in the last probeStaleAfter (2m).
Unreachable addresses are removed from the registry. A pluggable
ModelProber lets tests substitute a fake without standing up gRPC.
DistributedBackendManager exposes DeleteBackendDetailed so the HTTP
handler can surface per-node outcomes ("2 succeeded, 1 queued") to the
UI in a follow-up commit; the existing DeleteBackend still returns
error-only for callers that don't care about node breakdown.
Multi-frontend safety: the state pass uses advisorylock.TryWithLockCtx
on a new key so N frontends coordinate — the same pattern the health
monitor and replica reconciler already rely on. Single-node mode runs
both passes inline (adapter is nil, state drain is a no-op).
Tests cover the upsert semantics, backoff math, the probe removing an
unreachable model but keeping a reachable one, and filtering by
probeStaleAfter.
* feat(ui): show cluster distribution of models in the System page
When a frontend restarted in distributed mode, models that workers had
already loaded weren't visible until the operator clicked into each node
manually — the /api/models/capabilities endpoint only knew about
configs on the frontend's filesystem, not the registry-backed truth.
/api/models/capabilities now joins in ListAllLoadedModels() when the
registry is active, returning loaded_on[] with node id/name/state/status
for each model. Models that live in the registry but lack a local config
(the actual ghosts, not recovered from the frontend's file cache) still
surface with source="registry-only" so operators can see and persist
them; without that emission they'd be invisible to this frontend.
Manage → Models replaces the old Running/Idle pill with a distribution
cell that lists the first three nodes the model is loaded on as chips
colored by state (green loaded, blue loading, amber anything else). On
wider clusters the remaining count collapses into a +N chip with a
title-attribute breakdown. Disabled / single-node behavior unchanged.
Adopted models get an extra "Adopted" ghost-icon chip with hover copy
explaining what it means and how to make it permanent.
Distributed mode also enables a 10s auto-refresh and a "Last synced Xs
ago" indicator next to the Update button so ghost rows drop off within
one reconcile tick after their owning process dies. Non-distributed
mode is untouched — no polling, no cell-stack, same old Running/Idle.
* feat(ui): NodeDistributionChip — shared per-node attribution component
Large clusters were going to break the Manage → Backends Nodes column:
the old inline logic rendered every node as a badge and would shred the
layout at >10 workers, plus the Manage → Models distribution cell had
copy-pasted its own slightly-different version.
NodeDistributionChip handles any cluster size with two render modes:
- small (≤3 nodes): inline chips of node names, colored by health.
- large: a single "on N nodes · M offline · K drift" summary chip;
clicking opens a Popover with a per-node table (name, status,
version, digest for backends; name, status, state for models).
Drift counting mirrors the backend's summarizeNodeDrift so the UI
number matches UpgradeInfo.NodeDrift. Digests are truncated to the
docker-style 12-char form with the full value preserved in the title.
Popover is a new general-purpose primitive: fixed positioning anchored
to the trigger, flips above when there's no room below, closes on
outside-click or Escape, returns focus to the trigger. Uses .card as
its surface so theming is inherited. Also useful for a future
labels-editor popup and the user menu.
Manage.jsx drops its duplicated inline Nodes-column + loaded_on cell
and uses the shared chip with context="backends" / "models"
respectively. Delete code removes ~40 lines of ad-hoc logic.
* feat(ui): shared FilterBar across the System page tabs
The Backends gallery had a nice search + chip + toggle strip; the System
page had nothing, so the two surfaces felt like different apps. Lift the
pattern into a reusable FilterBar and wire both System tabs through it.
New component core/http/react-ui/src/components/FilterBar.jsx renders a
search input, a role="tablist" chip row (aria-selected for a11y), and
optional toggles / right slot. Chips support an optional `count` which
the System page uses to show "User 3", "Updates 1" etc.
System Models tab: search by id or backend; chips for
All/Running/Idle/Disabled/Pinned plus a conditional Distributed chip in
distributed mode. "Last synced" + Update button live in the right slot.
System Backends tab: search by name/alias/meta-backend-for; chips for
All/User/System/Meta plus conditional Updates / Offline-nodes chips
when relevant. The old ad-hoc "Updates only" toggle from the upgrade
banner folded into the Updates chip — one source of truth for that
filter. Offline chip only appears in distributed mode when at least
one backend has an unhealthy node, so the chip row stays quiet on
healthy clusters.
Filter state persists in URL query params (mq/mf/bq/bf) so deep links
and tab switches keep the operator's filter context instead of
resetting every time.
Also adds an "Adopted" distribution path: when a model in
/api/models/capabilities carries source="registry-only" (discovered on
a worker but not configured locally), the Models tab shows a ghost chip
labelled "Adopted" with hover copy explaining how to persist it — this
is what closes the loop on the ghost-model story end-to-end.
2026-04-19 15:55:53 +00:00
|
|
|
|
.badge-accent {
|
|
|
|
|
|
background: var(--color-accent-light);
|
|
|
|
|
|
color: var(--color-accent);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Horizontal row of badges used inside table cells — consistent spacing so
|
|
|
|
|
|
cells line up regardless of how many badges are present. */
|
|
|
|
|
|
.badge-row {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Vertically stacked cell content (e.g. version + update chip + drift chip).
|
|
|
|
|
|
Keeps rows readable at scale without inline style={{...}} everywhere. */
|
|
|
|
|
|
.cell-stack {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cell-mono {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
font-family: var(--font-mono);
|
feat(distributed): sync state with frontends, better backend management reporting (#9426)
* fix(distributed): detect backend upgrades across worker nodes
Before this change `DistributedBackendManager.CheckUpgrades` delegated to the
local manager, which read backends from the frontend filesystem. In
distributed deployments the frontend has no backends installed locally —
they live on workers — so the upgrade-detection loop never ran and the UI
silently never surfaced upgrades even when the gallery advertised newer
versions or digests.
Worker-side: NATS backend.list reply now carries Version, URI and Digest
for each installed backend (read from metadata.json).
Frontend-side: DistributedBackendManager.ListBackends aggregates per-node
refs (name, status, version, digest) instead of deduping, and CheckUpgrades
feeds that aggregation into gallery.CheckUpgradesAgainst — a new entrypoint
factored out of CheckBackendUpgrades so both paths share the same core
logic.
Cluster drift policy: when per-node version/digest tuples disagree, the
backend is flagged upgradeable regardless of whether any single node
matches the gallery, and UpgradeInfo.NodeDrift enumerates the outliers so
operators can see *why* it is out of sync. The next upgrade-all realigns
the cluster.
Tests cover: drift detection, unanimous-match (no upgrade), and the
empty-installed-version path that the old distributed code silently
missed.
* feat(ui): surface backend upgrades in the System page
The System page (Manage.jsx) only showed updates as a tiny inline arrow,
so operators routinely missed them. Port the Backend Gallery's upgrade UX
so System speaks the same visual language:
- Yellow banner at the top of the Backends tab when upgrades are pending,
with an "Upgrade all" button (serial fan-out, matches the gallery) and a
"Updates only" filter toggle.
- Warning pill (↑ N) next to the tab label so the count is glanceable even
when the banner is scrolled out of view.
- Per-row labeled "Upgrade to vX.Y" button (replaces the icon-only button
that silently flipped semantics between Reinstall and Upgrade), plus an
"Update available" badge in the new Version column.
- New columns: Version (with upgrade + drift chips), Nodes (per-node
attribution badges for distributed mode, degrading to a compact
"on N nodes · M offline" chip above three nodes), Installed (relative
time).
- System backends render a "Protected" chip instead of a bare "—" so rows
still align and the reason is obvious.
- Delete uses the softer btn-danger-ghost so rows don't scream red; the
ConfirmDialog still owns the "are you sure".
The upgrade checker also needed the same per-worker fix as the previous
commit: NewUpgradeChecker now takes a BackendManager getter so its
periodic runs call the distributed CheckUpgrades (which asks workers)
instead of the empty frontend filesystem. Without this the /api/backends/
upgrades endpoint stayed empty in distributed mode even with the protocol
change in place.
New CSS primitives — .upgrade-banner, .tab-pill, .badge-row, .cell-stack,
.cell-mono, .cell-muted, .row-actions, .btn-danger-ghost — all live in
App.css so other pages can adopt them without duplicating styles.
* feat(ui): polish the Nodes page so it reads like a product
The Nodes page was the biggest visual liability in distributed mode.
Rework the main dashboard surfaces in place without changing behavior:
StatCards: uniform height (96px min), left accent bar colored by the
metric's semantic (success/warning/error/primary), icon lives in a
36x36 soft-tinted chip top-right, value is left-aligned and large.
Grid auto-fills so the row doesn't collapse on narrow viewports. This
replaces the previous thin-bordered boxes with inconsistent heights.
Table rows: expandable rows now show a chevron cue on the left (rotates
on expand) so users know rows open. Status cell became a dedicated chip
with an LED-style halo dot instead of a bare bullet. Action buttons gained
labels — "Approve", "Resume", "Drain" — so the icons aren't doing all
the semantic work; the destructive remove action uses the softer
btn-danger-ghost variant so rows don't scream red, with the ConfirmDialog
still owning the real "are you sure". Applied cell-mono/cell-muted
utility classes so label chips and addresses share one spacing/font
grammar instead of re-declaring inline styles everywhere.
Expanded drawer: empty states for Loaded Models and Installed Backends
now render as a proper drawer-empty card (dashed border, icon, one-line
hint) instead of a plain muted string that read like broken formatting.
Tabs: three inline-styled buttons became the shared .tab class so they
inherit focus ring, hover state, and the rest of the design system —
matches the System page.
"Add more workers" toggle turned into a .nodes-add-worker dashed-border
button labelled "Register a new worker" (action voice) instead of a
chevron + muted link that operators kept mistaking for broken text.
New shared CSS primitives carry over to other pages:
.stat-grid + .stat-card, .row-chevron, .node-status, .drawer-empty,
.nodes-add-worker.
* feat(distributed): durable backend fan-out + state reconciliation
Two connected problems handled together:
1) Backend delete/install/upgrade used to silently skip non-healthy nodes,
so a delete during an outage left a zombie on the offline node once it
returned. The fan-out now records intent in a new pending_backend_ops
table before attempting the NATS round-trip. Currently-healthy nodes
get an immediate attempt; everyone else is queued. Unique index on
(node_id, backend, op) means reissuing the same operation refreshes
next_retry_at instead of stacking duplicates.
2) Loaded-model state could drift from reality: a worker OOM'd, got
killed, or restarted a backend process would leave a node_models row
claiming the model was still loaded, feeding ghost entries into the
/api/nodes/models listing and the router's scheduling decisions.
The existing ReplicaReconciler gains two new passes that run under a
fresh KeyStateReconciler advisory lock (non-blocking, so one wedged
frontend doesn't freeze the cluster):
- drainPendingBackendOps: retries queued ops whose next_retry_at has
passed on currently-healthy nodes. Success deletes the row; failure
bumps attempts and pushes next_retry_at out with exponential backoff
(30s → 15m cap). ErrNoResponders also marks the node unhealthy.
- probeLoadedModels: gRPC-HealthChecks addresses the DB thinks are
loaded but hasn't seen touched in the last probeStaleAfter (2m).
Unreachable addresses are removed from the registry. A pluggable
ModelProber lets tests substitute a fake without standing up gRPC.
DistributedBackendManager exposes DeleteBackendDetailed so the HTTP
handler can surface per-node outcomes ("2 succeeded, 1 queued") to the
UI in a follow-up commit; the existing DeleteBackend still returns
error-only for callers that don't care about node breakdown.
Multi-frontend safety: the state pass uses advisorylock.TryWithLockCtx
on a new key so N frontends coordinate — the same pattern the health
monitor and replica reconciler already rely on. Single-node mode runs
both passes inline (adapter is nil, state drain is a no-op).
Tests cover the upsert semantics, backoff math, the probe removing an
unreachable model but keeping a reachable one, and filtering by
probeStaleAfter.
* feat(ui): show cluster distribution of models in the System page
When a frontend restarted in distributed mode, models that workers had
already loaded weren't visible until the operator clicked into each node
manually — the /api/models/capabilities endpoint only knew about
configs on the frontend's filesystem, not the registry-backed truth.
/api/models/capabilities now joins in ListAllLoadedModels() when the
registry is active, returning loaded_on[] with node id/name/state/status
for each model. Models that live in the registry but lack a local config
(the actual ghosts, not recovered from the frontend's file cache) still
surface with source="registry-only" so operators can see and persist
them; without that emission they'd be invisible to this frontend.
Manage → Models replaces the old Running/Idle pill with a distribution
cell that lists the first three nodes the model is loaded on as chips
colored by state (green loaded, blue loading, amber anything else). On
wider clusters the remaining count collapses into a +N chip with a
title-attribute breakdown. Disabled / single-node behavior unchanged.
Adopted models get an extra "Adopted" ghost-icon chip with hover copy
explaining what it means and how to make it permanent.
Distributed mode also enables a 10s auto-refresh and a "Last synced Xs
ago" indicator next to the Update button so ghost rows drop off within
one reconcile tick after their owning process dies. Non-distributed
mode is untouched — no polling, no cell-stack, same old Running/Idle.
* feat(ui): NodeDistributionChip — shared per-node attribution component
Large clusters were going to break the Manage → Backends Nodes column:
the old inline logic rendered every node as a badge and would shred the
layout at >10 workers, plus the Manage → Models distribution cell had
copy-pasted its own slightly-different version.
NodeDistributionChip handles any cluster size with two render modes:
- small (≤3 nodes): inline chips of node names, colored by health.
- large: a single "on N nodes · M offline · K drift" summary chip;
clicking opens a Popover with a per-node table (name, status,
version, digest for backends; name, status, state for models).
Drift counting mirrors the backend's summarizeNodeDrift so the UI
number matches UpgradeInfo.NodeDrift. Digests are truncated to the
docker-style 12-char form with the full value preserved in the title.
Popover is a new general-purpose primitive: fixed positioning anchored
to the trigger, flips above when there's no room below, closes on
outside-click or Escape, returns focus to the trigger. Uses .card as
its surface so theming is inherited. Also useful for a future
labels-editor popup and the user menu.
Manage.jsx drops its duplicated inline Nodes-column + loaded_on cell
and uses the shared chip with context="backends" / "models"
respectively. Delete code removes ~40 lines of ad-hoc logic.
* feat(ui): shared FilterBar across the System page tabs
The Backends gallery had a nice search + chip + toggle strip; the System
page had nothing, so the two surfaces felt like different apps. Lift the
pattern into a reusable FilterBar and wire both System tabs through it.
New component core/http/react-ui/src/components/FilterBar.jsx renders a
search input, a role="tablist" chip row (aria-selected for a11y), and
optional toggles / right slot. Chips support an optional `count` which
the System page uses to show "User 3", "Updates 1" etc.
System Models tab: search by id or backend; chips for
All/Running/Idle/Disabled/Pinned plus a conditional Distributed chip in
distributed mode. "Last synced" + Update button live in the right slot.
System Backends tab: search by name/alias/meta-backend-for; chips for
All/User/System/Meta plus conditional Updates / Offline-nodes chips
when relevant. The old ad-hoc "Updates only" toggle from the upgrade
banner folded into the Updates chip — one source of truth for that
filter. Offline chip only appears in distributed mode when at least
one backend has an unhealthy node, so the chip row stays quiet on
healthy clusters.
Filter state persists in URL query params (mq/mf/bq/bf) so deep links
and tab switches keep the operator's filter context instead of
resetting every time.
Also adds an "Adopted" distribution path: when a model in
/api/models/capabilities carries source="registry-only" (discovered on
a worker but not configured locally), the Models tab shows a ghost chip
labelled "Adopted" with hover copy explaining how to persist it — this
is what closes the loop on the ghost-model story end-to-end.
2026-04-19 15:55:53 +00:00
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cell-muted {
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cell-subtle {
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: 400;
|
|
|
|
|
|
margin-left: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cell-name {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
.cell-name > i {
|
|
|
|
|
|
color: var(--color-accent);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.row-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Softer delete button for dense tables — the destructive confirm dialog
|
|
|
|
|
|
already owns the "are you sure" affordance, so the button itself doesn't
|
|
|
|
|
|
need to scream. Keeps the delete red readable without dominating rows. */
|
|
|
|
|
|
.btn.btn-danger-ghost {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
border-color: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
.btn.btn-danger-ghost:hover:not(:disabled) {
|
|
|
|
|
|
background: var(--color-error-light);
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
border-color: var(--color-error-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Small count pill used inside tabs ("(3) ↑ 2") so update counts are
|
|
|
|
|
|
glanceable without extra rows of UI. */
|
|
|
|
|
|
.tab-pill {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 3px;
|
|
|
|
|
|
margin-left: 6px;
|
|
|
|
|
|
padding: 1px 6px;
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
.tab-pill--warning {
|
|
|
|
|
|
background: var(--color-warning-light);
|
|
|
|
|
|
color: var(--color-warning);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Stat cards — uniform-height cluster metrics for the Nodes dashboard.
|
|
|
|
|
|
Left accent bar ties the color to the metric's semantic (success/warning/
|
|
|
|
|
|
error/primary), icon chip sits top-right, value is left-aligned and
|
|
|
|
|
|
prominent so you can scan a row of cards without reading labels. */
|
|
|
|
|
|
.stat-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
margin-bottom: var(--spacing-xl);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
min-height: 96px;
|
|
|
|
|
|
background: var(--color-bg-raised, var(--color-bg-secondary));
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
transition: transform var(--duration-fast) var(--ease-default),
|
|
|
|
|
|
box-shadow var(--duration-fast) var(--ease-default),
|
|
|
|
|
|
border-color var(--duration-fast) var(--ease-default);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-card::before {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 0; top: 0; bottom: 0;
|
|
|
|
|
|
width: 3px;
|
|
|
|
|
|
background: var(--stat-accent, var(--color-border-subtle));
|
|
|
|
|
|
transition: background var(--duration-fast) var(--ease-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-card:hover {
|
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
|
box-shadow: var(--shadow-sm);
|
|
|
|
|
|
border-color: var(--color-border);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.stat-card__body {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-card__label {
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
white-space: normal;
|
|
|
|
|
|
line-height: 1.2;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-card__value {
|
|
|
|
|
|
font-size: var(--text-2xl);
|
|
|
|
|
|
font-weight: 600;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
font-family: var(--font-mono);
|
feat(distributed): sync state with frontends, better backend management reporting (#9426)
* fix(distributed): detect backend upgrades across worker nodes
Before this change `DistributedBackendManager.CheckUpgrades` delegated to the
local manager, which read backends from the frontend filesystem. In
distributed deployments the frontend has no backends installed locally —
they live on workers — so the upgrade-detection loop never ran and the UI
silently never surfaced upgrades even when the gallery advertised newer
versions or digests.
Worker-side: NATS backend.list reply now carries Version, URI and Digest
for each installed backend (read from metadata.json).
Frontend-side: DistributedBackendManager.ListBackends aggregates per-node
refs (name, status, version, digest) instead of deduping, and CheckUpgrades
feeds that aggregation into gallery.CheckUpgradesAgainst — a new entrypoint
factored out of CheckBackendUpgrades so both paths share the same core
logic.
Cluster drift policy: when per-node version/digest tuples disagree, the
backend is flagged upgradeable regardless of whether any single node
matches the gallery, and UpgradeInfo.NodeDrift enumerates the outliers so
operators can see *why* it is out of sync. The next upgrade-all realigns
the cluster.
Tests cover: drift detection, unanimous-match (no upgrade), and the
empty-installed-version path that the old distributed code silently
missed.
* feat(ui): surface backend upgrades in the System page
The System page (Manage.jsx) only showed updates as a tiny inline arrow,
so operators routinely missed them. Port the Backend Gallery's upgrade UX
so System speaks the same visual language:
- Yellow banner at the top of the Backends tab when upgrades are pending,
with an "Upgrade all" button (serial fan-out, matches the gallery) and a
"Updates only" filter toggle.
- Warning pill (↑ N) next to the tab label so the count is glanceable even
when the banner is scrolled out of view.
- Per-row labeled "Upgrade to vX.Y" button (replaces the icon-only button
that silently flipped semantics between Reinstall and Upgrade), plus an
"Update available" badge in the new Version column.
- New columns: Version (with upgrade + drift chips), Nodes (per-node
attribution badges for distributed mode, degrading to a compact
"on N nodes · M offline" chip above three nodes), Installed (relative
time).
- System backends render a "Protected" chip instead of a bare "—" so rows
still align and the reason is obvious.
- Delete uses the softer btn-danger-ghost so rows don't scream red; the
ConfirmDialog still owns the "are you sure".
The upgrade checker also needed the same per-worker fix as the previous
commit: NewUpgradeChecker now takes a BackendManager getter so its
periodic runs call the distributed CheckUpgrades (which asks workers)
instead of the empty frontend filesystem. Without this the /api/backends/
upgrades endpoint stayed empty in distributed mode even with the protocol
change in place.
New CSS primitives — .upgrade-banner, .tab-pill, .badge-row, .cell-stack,
.cell-mono, .cell-muted, .row-actions, .btn-danger-ghost — all live in
App.css so other pages can adopt them without duplicating styles.
* feat(ui): polish the Nodes page so it reads like a product
The Nodes page was the biggest visual liability in distributed mode.
Rework the main dashboard surfaces in place without changing behavior:
StatCards: uniform height (96px min), left accent bar colored by the
metric's semantic (success/warning/error/primary), icon lives in a
36x36 soft-tinted chip top-right, value is left-aligned and large.
Grid auto-fills so the row doesn't collapse on narrow viewports. This
replaces the previous thin-bordered boxes with inconsistent heights.
Table rows: expandable rows now show a chevron cue on the left (rotates
on expand) so users know rows open. Status cell became a dedicated chip
with an LED-style halo dot instead of a bare bullet. Action buttons gained
labels — "Approve", "Resume", "Drain" — so the icons aren't doing all
the semantic work; the destructive remove action uses the softer
btn-danger-ghost variant so rows don't scream red, with the ConfirmDialog
still owning the real "are you sure". Applied cell-mono/cell-muted
utility classes so label chips and addresses share one spacing/font
grammar instead of re-declaring inline styles everywhere.
Expanded drawer: empty states for Loaded Models and Installed Backends
now render as a proper drawer-empty card (dashed border, icon, one-line
hint) instead of a plain muted string that read like broken formatting.
Tabs: three inline-styled buttons became the shared .tab class so they
inherit focus ring, hover state, and the rest of the design system —
matches the System page.
"Add more workers" toggle turned into a .nodes-add-worker dashed-border
button labelled "Register a new worker" (action voice) instead of a
chevron + muted link that operators kept mistaking for broken text.
New shared CSS primitives carry over to other pages:
.stat-grid + .stat-card, .row-chevron, .node-status, .drawer-empty,
.nodes-add-worker.
* feat(distributed): durable backend fan-out + state reconciliation
Two connected problems handled together:
1) Backend delete/install/upgrade used to silently skip non-healthy nodes,
so a delete during an outage left a zombie on the offline node once it
returned. The fan-out now records intent in a new pending_backend_ops
table before attempting the NATS round-trip. Currently-healthy nodes
get an immediate attempt; everyone else is queued. Unique index on
(node_id, backend, op) means reissuing the same operation refreshes
next_retry_at instead of stacking duplicates.
2) Loaded-model state could drift from reality: a worker OOM'd, got
killed, or restarted a backend process would leave a node_models row
claiming the model was still loaded, feeding ghost entries into the
/api/nodes/models listing and the router's scheduling decisions.
The existing ReplicaReconciler gains two new passes that run under a
fresh KeyStateReconciler advisory lock (non-blocking, so one wedged
frontend doesn't freeze the cluster):
- drainPendingBackendOps: retries queued ops whose next_retry_at has
passed on currently-healthy nodes. Success deletes the row; failure
bumps attempts and pushes next_retry_at out with exponential backoff
(30s → 15m cap). ErrNoResponders also marks the node unhealthy.
- probeLoadedModels: gRPC-HealthChecks addresses the DB thinks are
loaded but hasn't seen touched in the last probeStaleAfter (2m).
Unreachable addresses are removed from the registry. A pluggable
ModelProber lets tests substitute a fake without standing up gRPC.
DistributedBackendManager exposes DeleteBackendDetailed so the HTTP
handler can surface per-node outcomes ("2 succeeded, 1 queued") to the
UI in a follow-up commit; the existing DeleteBackend still returns
error-only for callers that don't care about node breakdown.
Multi-frontend safety: the state pass uses advisorylock.TryWithLockCtx
on a new key so N frontends coordinate — the same pattern the health
monitor and replica reconciler already rely on. Single-node mode runs
both passes inline (adapter is nil, state drain is a no-op).
Tests cover the upsert semantics, backoff math, the probe removing an
unreachable model but keeping a reachable one, and filtering by
probeStaleAfter.
* feat(ui): show cluster distribution of models in the System page
When a frontend restarted in distributed mode, models that workers had
already loaded weren't visible until the operator clicked into each node
manually — the /api/models/capabilities endpoint only knew about
configs on the frontend's filesystem, not the registry-backed truth.
/api/models/capabilities now joins in ListAllLoadedModels() when the
registry is active, returning loaded_on[] with node id/name/state/status
for each model. Models that live in the registry but lack a local config
(the actual ghosts, not recovered from the frontend's file cache) still
surface with source="registry-only" so operators can see and persist
them; without that emission they'd be invisible to this frontend.
Manage → Models replaces the old Running/Idle pill with a distribution
cell that lists the first three nodes the model is loaded on as chips
colored by state (green loaded, blue loading, amber anything else). On
wider clusters the remaining count collapses into a +N chip with a
title-attribute breakdown. Disabled / single-node behavior unchanged.
Adopted models get an extra "Adopted" ghost-icon chip with hover copy
explaining what it means and how to make it permanent.
Distributed mode also enables a 10s auto-refresh and a "Last synced Xs
ago" indicator next to the Update button so ghost rows drop off within
one reconcile tick after their owning process dies. Non-distributed
mode is untouched — no polling, no cell-stack, same old Running/Idle.
* feat(ui): NodeDistributionChip — shared per-node attribution component
Large clusters were going to break the Manage → Backends Nodes column:
the old inline logic rendered every node as a badge and would shred the
layout at >10 workers, plus the Manage → Models distribution cell had
copy-pasted its own slightly-different version.
NodeDistributionChip handles any cluster size with two render modes:
- small (≤3 nodes): inline chips of node names, colored by health.
- large: a single "on N nodes · M offline · K drift" summary chip;
clicking opens a Popover with a per-node table (name, status,
version, digest for backends; name, status, state for models).
Drift counting mirrors the backend's summarizeNodeDrift so the UI
number matches UpgradeInfo.NodeDrift. Digests are truncated to the
docker-style 12-char form with the full value preserved in the title.
Popover is a new general-purpose primitive: fixed positioning anchored
to the trigger, flips above when there's no room below, closes on
outside-click or Escape, returns focus to the trigger. Uses .card as
its surface so theming is inherited. Also useful for a future
labels-editor popup and the user menu.
Manage.jsx drops its duplicated inline Nodes-column + loaded_on cell
and uses the shared chip with context="backends" / "models"
respectively. Delete code removes ~40 lines of ad-hoc logic.
* feat(ui): shared FilterBar across the System page tabs
The Backends gallery had a nice search + chip + toggle strip; the System
page had nothing, so the two surfaces felt like different apps. Lift the
pattern into a reusable FilterBar and wire both System tabs through it.
New component core/http/react-ui/src/components/FilterBar.jsx renders a
search input, a role="tablist" chip row (aria-selected for a11y), and
optional toggles / right slot. Chips support an optional `count` which
the System page uses to show "User 3", "Updates 1" etc.
System Models tab: search by id or backend; chips for
All/Running/Idle/Disabled/Pinned plus a conditional Distributed chip in
distributed mode. "Last synced" + Update button live in the right slot.
System Backends tab: search by name/alias/meta-backend-for; chips for
All/User/System/Meta plus conditional Updates / Offline-nodes chips
when relevant. The old ad-hoc "Updates only" toggle from the upgrade
banner folded into the Updates chip — one source of truth for that
filter. Offline chip only appears in distributed mode when at least
one backend has an unhealthy node, so the chip row stays quiet on
healthy clusters.
Filter state persists in URL query params (mq/mf/bq/bf) so deep links
and tab switches keep the operator's filter context instead of
resetting every time.
Also adds an "Adopted" distribution path: when a model in
/api/models/capabilities carries source="registry-only" (discovered on
a worker but not configured locally), the Models tab shows a ghost chip
labelled "Adopted" with hover copy explaining how to persist it — this
is what closes the loop on the ghost-model story end-to-end.
2026-04-19 15:55:53 +00:00
|
|
|
|
line-height: 1;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-card__icon {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 36px;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
background: color-mix(in srgb, var(--stat-accent, var(--color-text-muted)) 12%, transparent);
|
|
|
|
|
|
color: var(--stat-accent, var(--color-text-muted));
|
|
|
|
|
|
font-size: var(--text-lg);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Subtle "Register a new worker" trigger replacing the broken-text chevron
|
|
|
|
|
|
link. Still opens the same hint card — just reads like a button now. */
|
|
|
|
|
|
.nodes-add-worker {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: 1px dashed var(--color-border);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
transition: background var(--duration-fast) var(--ease-default),
|
|
|
|
|
|
border-color var(--duration-fast) var(--ease-default),
|
|
|
|
|
|
color var(--duration-fast) var(--ease-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
.nodes-add-worker:hover {
|
|
|
|
|
|
background: var(--color-bg-raised, var(--color-bg-secondary));
|
|
|
|
|
|
border-color: var(--color-border-strong);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Shared FilterBar layout — search strip + chip row + toggle strip. Lives
|
|
|
|
|
|
outside the .filter-bar chip row so the padding and wrapping behavior is
|
|
|
|
|
|
consistent between the Backends gallery and the System tabs. */
|
|
|
|
|
|
.filter-bar-group {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.filter-bar-group__search {
|
|
|
|
|
|
min-width: 200px;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
.filter-bar-group__row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.filter-bar-group__right {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
padding-left: var(--spacing-md);
|
|
|
|
|
|
border-left: 1px solid var(--color-border-subtle);
|
2026-04-26 19:34:54 +00:00
|
|
|
|
margin-left: auto;
|
feat(distributed): sync state with frontends, better backend management reporting (#9426)
* fix(distributed): detect backend upgrades across worker nodes
Before this change `DistributedBackendManager.CheckUpgrades` delegated to the
local manager, which read backends from the frontend filesystem. In
distributed deployments the frontend has no backends installed locally —
they live on workers — so the upgrade-detection loop never ran and the UI
silently never surfaced upgrades even when the gallery advertised newer
versions or digests.
Worker-side: NATS backend.list reply now carries Version, URI and Digest
for each installed backend (read from metadata.json).
Frontend-side: DistributedBackendManager.ListBackends aggregates per-node
refs (name, status, version, digest) instead of deduping, and CheckUpgrades
feeds that aggregation into gallery.CheckUpgradesAgainst — a new entrypoint
factored out of CheckBackendUpgrades so both paths share the same core
logic.
Cluster drift policy: when per-node version/digest tuples disagree, the
backend is flagged upgradeable regardless of whether any single node
matches the gallery, and UpgradeInfo.NodeDrift enumerates the outliers so
operators can see *why* it is out of sync. The next upgrade-all realigns
the cluster.
Tests cover: drift detection, unanimous-match (no upgrade), and the
empty-installed-version path that the old distributed code silently
missed.
* feat(ui): surface backend upgrades in the System page
The System page (Manage.jsx) only showed updates as a tiny inline arrow,
so operators routinely missed them. Port the Backend Gallery's upgrade UX
so System speaks the same visual language:
- Yellow banner at the top of the Backends tab when upgrades are pending,
with an "Upgrade all" button (serial fan-out, matches the gallery) and a
"Updates only" filter toggle.
- Warning pill (↑ N) next to the tab label so the count is glanceable even
when the banner is scrolled out of view.
- Per-row labeled "Upgrade to vX.Y" button (replaces the icon-only button
that silently flipped semantics between Reinstall and Upgrade), plus an
"Update available" badge in the new Version column.
- New columns: Version (with upgrade + drift chips), Nodes (per-node
attribution badges for distributed mode, degrading to a compact
"on N nodes · M offline" chip above three nodes), Installed (relative
time).
- System backends render a "Protected" chip instead of a bare "—" so rows
still align and the reason is obvious.
- Delete uses the softer btn-danger-ghost so rows don't scream red; the
ConfirmDialog still owns the "are you sure".
The upgrade checker also needed the same per-worker fix as the previous
commit: NewUpgradeChecker now takes a BackendManager getter so its
periodic runs call the distributed CheckUpgrades (which asks workers)
instead of the empty frontend filesystem. Without this the /api/backends/
upgrades endpoint stayed empty in distributed mode even with the protocol
change in place.
New CSS primitives — .upgrade-banner, .tab-pill, .badge-row, .cell-stack,
.cell-mono, .cell-muted, .row-actions, .btn-danger-ghost — all live in
App.css so other pages can adopt them without duplicating styles.
* feat(ui): polish the Nodes page so it reads like a product
The Nodes page was the biggest visual liability in distributed mode.
Rework the main dashboard surfaces in place without changing behavior:
StatCards: uniform height (96px min), left accent bar colored by the
metric's semantic (success/warning/error/primary), icon lives in a
36x36 soft-tinted chip top-right, value is left-aligned and large.
Grid auto-fills so the row doesn't collapse on narrow viewports. This
replaces the previous thin-bordered boxes with inconsistent heights.
Table rows: expandable rows now show a chevron cue on the left (rotates
on expand) so users know rows open. Status cell became a dedicated chip
with an LED-style halo dot instead of a bare bullet. Action buttons gained
labels — "Approve", "Resume", "Drain" — so the icons aren't doing all
the semantic work; the destructive remove action uses the softer
btn-danger-ghost variant so rows don't scream red, with the ConfirmDialog
still owning the real "are you sure". Applied cell-mono/cell-muted
utility classes so label chips and addresses share one spacing/font
grammar instead of re-declaring inline styles everywhere.
Expanded drawer: empty states for Loaded Models and Installed Backends
now render as a proper drawer-empty card (dashed border, icon, one-line
hint) instead of a plain muted string that read like broken formatting.
Tabs: three inline-styled buttons became the shared .tab class so they
inherit focus ring, hover state, and the rest of the design system —
matches the System page.
"Add more workers" toggle turned into a .nodes-add-worker dashed-border
button labelled "Register a new worker" (action voice) instead of a
chevron + muted link that operators kept mistaking for broken text.
New shared CSS primitives carry over to other pages:
.stat-grid + .stat-card, .row-chevron, .node-status, .drawer-empty,
.nodes-add-worker.
* feat(distributed): durable backend fan-out + state reconciliation
Two connected problems handled together:
1) Backend delete/install/upgrade used to silently skip non-healthy nodes,
so a delete during an outage left a zombie on the offline node once it
returned. The fan-out now records intent in a new pending_backend_ops
table before attempting the NATS round-trip. Currently-healthy nodes
get an immediate attempt; everyone else is queued. Unique index on
(node_id, backend, op) means reissuing the same operation refreshes
next_retry_at instead of stacking duplicates.
2) Loaded-model state could drift from reality: a worker OOM'd, got
killed, or restarted a backend process would leave a node_models row
claiming the model was still loaded, feeding ghost entries into the
/api/nodes/models listing and the router's scheduling decisions.
The existing ReplicaReconciler gains two new passes that run under a
fresh KeyStateReconciler advisory lock (non-blocking, so one wedged
frontend doesn't freeze the cluster):
- drainPendingBackendOps: retries queued ops whose next_retry_at has
passed on currently-healthy nodes. Success deletes the row; failure
bumps attempts and pushes next_retry_at out with exponential backoff
(30s → 15m cap). ErrNoResponders also marks the node unhealthy.
- probeLoadedModels: gRPC-HealthChecks addresses the DB thinks are
loaded but hasn't seen touched in the last probeStaleAfter (2m).
Unreachable addresses are removed from the registry. A pluggable
ModelProber lets tests substitute a fake without standing up gRPC.
DistributedBackendManager exposes DeleteBackendDetailed so the HTTP
handler can surface per-node outcomes ("2 succeeded, 1 queued") to the
UI in a follow-up commit; the existing DeleteBackend still returns
error-only for callers that don't care about node breakdown.
Multi-frontend safety: the state pass uses advisorylock.TryWithLockCtx
on a new key so N frontends coordinate — the same pattern the health
monitor and replica reconciler already rely on. Single-node mode runs
both passes inline (adapter is nil, state drain is a no-op).
Tests cover the upsert semantics, backoff math, the probe removing an
unreachable model but keeping a reachable one, and filtering by
probeStaleAfter.
* feat(ui): show cluster distribution of models in the System page
When a frontend restarted in distributed mode, models that workers had
already loaded weren't visible until the operator clicked into each node
manually — the /api/models/capabilities endpoint only knew about
configs on the frontend's filesystem, not the registry-backed truth.
/api/models/capabilities now joins in ListAllLoadedModels() when the
registry is active, returning loaded_on[] with node id/name/state/status
for each model. Models that live in the registry but lack a local config
(the actual ghosts, not recovered from the frontend's file cache) still
surface with source="registry-only" so operators can see and persist
them; without that emission they'd be invisible to this frontend.
Manage → Models replaces the old Running/Idle pill with a distribution
cell that lists the first three nodes the model is loaded on as chips
colored by state (green loaded, blue loading, amber anything else). On
wider clusters the remaining count collapses into a +N chip with a
title-attribute breakdown. Disabled / single-node behavior unchanged.
Adopted models get an extra "Adopted" ghost-icon chip with hover copy
explaining what it means and how to make it permanent.
Distributed mode also enables a 10s auto-refresh and a "Last synced Xs
ago" indicator next to the Update button so ghost rows drop off within
one reconcile tick after their owning process dies. Non-distributed
mode is untouched — no polling, no cell-stack, same old Running/Idle.
* feat(ui): NodeDistributionChip — shared per-node attribution component
Large clusters were going to break the Manage → Backends Nodes column:
the old inline logic rendered every node as a badge and would shred the
layout at >10 workers, plus the Manage → Models distribution cell had
copy-pasted its own slightly-different version.
NodeDistributionChip handles any cluster size with two render modes:
- small (≤3 nodes): inline chips of node names, colored by health.
- large: a single "on N nodes · M offline · K drift" summary chip;
clicking opens a Popover with a per-node table (name, status,
version, digest for backends; name, status, state for models).
Drift counting mirrors the backend's summarizeNodeDrift so the UI
number matches UpgradeInfo.NodeDrift. Digests are truncated to the
docker-style 12-char form with the full value preserved in the title.
Popover is a new general-purpose primitive: fixed positioning anchored
to the trigger, flips above when there's no room below, closes on
outside-click or Escape, returns focus to the trigger. Uses .card as
its surface so theming is inherited. Also useful for a future
labels-editor popup and the user menu.
Manage.jsx drops its duplicated inline Nodes-column + loaded_on cell
and uses the shared chip with context="backends" / "models"
respectively. Delete code removes ~40 lines of ad-hoc logic.
* feat(ui): shared FilterBar across the System page tabs
The Backends gallery had a nice search + chip + toggle strip; the System
page had nothing, so the two surfaces felt like different apps. Lift the
pattern into a reusable FilterBar and wire both System tabs through it.
New component core/http/react-ui/src/components/FilterBar.jsx renders a
search input, a role="tablist" chip row (aria-selected for a11y), and
optional toggles / right slot. Chips support an optional `count` which
the System page uses to show "User 3", "Updates 1" etc.
System Models tab: search by id or backend; chips for
All/Running/Idle/Disabled/Pinned plus a conditional Distributed chip in
distributed mode. "Last synced" + Update button live in the right slot.
System Backends tab: search by name/alias/meta-backend-for; chips for
All/User/System/Meta plus conditional Updates / Offline-nodes chips
when relevant. The old ad-hoc "Updates only" toggle from the upgrade
banner folded into the Updates chip — one source of truth for that
filter. Offline chip only appears in distributed mode when at least
one backend has an unhealthy node, so the chip row stays quiet on
healthy clusters.
Filter state persists in URL query params (mq/mf/bq/bf) so deep links
and tab switches keep the operator's filter context instead of
resetting every time.
Also adds an "Adopted" distribution path: when a model in
/api/models/capabilities carries source="registry-only" (discovered on
a worker but not configured locally), the Models tab shows a ghost chip
labelled "Adopted" with hover copy explaining how to persist it — this
is what closes the loop on the ghost-model story end-to-end.
2026-04-19 15:55:53 +00:00
|
|
|
|
}
|
|
|
|
|
|
.filter-bar-group__toggle {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.filter-btn__count {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
margin-left: 6px;
|
|
|
|
|
|
min-width: 18px;
|
|
|
|
|
|
padding: 0 5px;
|
|
|
|
|
|
background: color-mix(in srgb, currentColor 18%, transparent);
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
font-size: 0.625rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Popover — floating surface anchored to a trigger element. Uses the .card
|
|
|
|
|
|
base so theming is free, adds z-index + fixed-position + scroll cap so it
|
|
|
|
|
|
behaves on tables with many rows. Kept deliberately unstyled beyond that
|
|
|
|
|
|
— content is expected to provide its own header/body structure. */
|
|
|
|
|
|
.popover {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
z-index: 200;
|
|
|
|
|
|
min-width: 260px;
|
|
|
|
|
|
max-width: min(420px, 95vw);
|
|
|
|
|
|
max-height: min(420px, 70vh);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
padding: 0; /* sections provide their own padding */
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
box-shadow: var(--shadow-lg);
|
|
|
|
|
|
animation: popoverIn var(--duration-fast) var(--ease-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes popoverIn {
|
|
|
|
|
|
from { opacity: 0; transform: translateY(-4px) scale(0.98); }
|
|
|
|
|
|
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.popover__header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.popover__scroll {
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.popover__table {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
.popover__table th {
|
|
|
|
|
|
position: sticky;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
background: var(--color-bg-raised, var(--color-bg-secondary));
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Inline-table chip trigger — looks like a badge but is a button (cursor,
|
|
|
|
|
|
focus ring inherited from global :focus-visible). */
|
|
|
|
|
|
.chip-trigger {
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chip-trigger:hover {
|
|
|
|
|
|
filter: brightness(1.08);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Truncate + ellipsize a long cell (e.g. OCI digest) without breaking the
|
|
|
|
|
|
table layout. Tooltip preserves the full value. */
|
|
|
|
|
|
.cell-truncate {
|
|
|
|
|
|
max-width: 160px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Compact empty-state used inside expanded drawer sections (e.g. "No
|
|
|
|
|
|
models loaded on this node"). Dimmer than the page-level .empty-state
|
|
|
|
|
|
because it lives inside another container and shouldn't compete with
|
|
|
|
|
|
the row's primary content. */
|
|
|
|
|
|
.drawer-empty {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
border: 1px dashed var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.drawer-empty > i {
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
opacity: 0.8;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(distributed): support multiple replicas of one model on the same node (#9583)
* feat(distributed): support multiple replicas of one model on the same node
The distributed scheduler implicitly assumed `(node_id, model_name)` was
unique, but the schema didn't enforce it and the worker keyed all gRPC
processes by model name alone. With `MinReplicas=2` against a single
worker, the reconciler "scaled up" every 30s but the registry never
advanced past 1 row — the worker re-loaded the model in-place every tick
until VRAM fragmented and the gRPC process died.
This change introduces multi-replica-per-node as a first-class concept,
with capacity-aware scheduling, a circuit breaker, and VRAM
soft-reservation. Operators can declare per-node capacity via the worker
flag `--max-replicas-per-model` (mirrored as auto-label
`node.replica-slots=N`) or override per-node from the UI.
* Schema: BackendNode gains MaxReplicasPerModel (default 1) and
ReservedVRAM. NodeModel gains ReplicaIndex (composite with node_id +
model_name). ModelSchedulingConfig gains UnsatisfiableUntil/Ticks for
the reconciler circuit breaker.
* Registry: replica_index threaded through SetNodeModel, RemoveNodeModel,
IncrementInFlight, DecrementInFlight, TouchNodeModel, GetNodeModel,
SetNodeModelLoadInfo and the InFlightTrackingClient. New helpers:
CountReplicasOnNode, NextFreeReplicaIndex (with ErrNoFreeSlot),
RemoveAllNodeModelReplicas, FindNodesWithFreeSlot,
ClusterCapacityForModel, ReserveVRAM/ReleaseVRAM (atomic UPDATE with
ErrInsufficientVRAM), and the unsatisfiable-flag CRUD.
* Worker: processKey now `<modelID>#<replicaIndex>` so concurrent loads
of the same model land on distinct ports. Adds CLI flag
--max-replicas-per-model (env LOCALAI_MAX_REPLICAS_PER_MODEL, default 1)
and emits the auto-label.
* Router: scheduleNewModel filters candidates by free slot, allocates the
replica index, and soft-reserves VRAM before installing the backend.
evictLRUAndFreeNode now deletes the targeted row by ID instead of all
replicas of the model on the node — fixes a latent bug where evicting
one replica orphaned its siblings.
* Reconciler: caps scale-up at ClusterCapacityForModel so a misconfig
(MinReplicas > capacity) doesn't loop forever. After 3 consecutive
ticks of capacity==0 it sets UnsatisfiableUntil for a 5m cooldown and
emits a warning. ClearAllUnsatisfiable fires from Register,
ApproveNode, SetNodeLabel(s), RemoveNodeLabel and
UpdateMaxReplicasPerModel so a new node joining or label changes wake
the reconciler immediately. scaleDownIdle removes highest-replica-index
first to keep slots compact.
* Heartbeat resets reserved_vram to 0 — worker is the source of truth
for actual free VRAM; the reservation is only for the in-tick race
window between two scheduling decisions.
* Probe path (reconciler.probeLoadedModels and health.doCheckAll) now
pass the row's replica_index to RemoveNodeModel so an unreachable
replica doesn't orphan healthy siblings.
* Admin override: PUT /api/nodes/:id/max-replicas-per-model sets a
sticky override (preserved across worker re-registration). DELETE
clears the override so the worker's flag applies again on next
register. Required because Kong defaults the worker flag to 1, so
every worker restart would have silently reverted the UI value.
* React UI: always-visible slot badge on the node row (muted at default
1, accented when >1); inline editor in the expanded drawer with
pencil-to-edit, Save/Cancel, Esc/Enter, "(override)" indicator when
the value is admin-set, and a "Reset" button to hand control back to
the worker. Soft confirm when shrinking the cap below the count of
loaded replicas. Scheduling rules table gets an "Unsatisfiable until
HH:MM" status badge surfacing the cooldown.
* node.replica-slots filtered out of the labels strip on the row to
avoid duplicating the slot badge.
23 new Ginkgo specs (registry, reconciler, inflight, health) cover:
multi-replica row independence, RemoveNodeModel of one replica
preserving siblings, NextFreeReplicaIndex slot allocation including
ErrNoFreeSlot, capacity-gated scale-up with circuit breaker tripping
and recovery on Register, scheduleDownIdle ordering, ClusterCapacity
math, ReserveVRAM admission gating, Heartbeat reset, override survival
across worker re-registration, and ResetMaxReplicasPerModel handing
control back. Plus 8 stdlib tests for the worker processKey / CLI /
auto-label.
Closes the flap reproduced on Qwen3.6-35B against the nvidia-thor
worker (single 128 GiB node, MinReplicas=2): the reconciler now caps
the scale-up at the cluster's actual capacity instead of looping.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: claude-code:opus-4-7 [Read] [Edit] [Bash] [Skill:critique] [Skill:audit] [Skill:polish] [Skill:golang-testing]
* refactor(react-ui/nodes): tighten capacity editor copy + adopt ActionMenu for row actions
* Capacity editor hint trimmed from operator-doc-style ("Sourced from
the worker's `--max-replicas-per-model` flag. Changing it here makes it
a sticky admin override that survives worker restarts." → "Saved
values stick across worker restarts.") and the override-state copy
similarly compressed. The full mechanic is no longer needed in the UI
— the override pill carries the meaning and the docs cover the rest.
* Node row actions migrated from an inline cluster of icon buttons
(Drain / Resume / Trash) to the kebab ActionMenu used by /manage for
per-row model actions, so dense Nodes tables stay clean. Approve
stays as a prominent primary button — it's a stateful admission gate,
not a routine action, and elevating it matches how /manage surfaces
install-time decisions outside the menu.
* The expanded drawer's Labels section now filters node.replica-slots
out of the editable label list. The label is owned by the Capacity
editor above; surfacing it again as an editable label invited
confusion (the Capacity save would clobber any direct edit).
Both backend and agent workers benefit — they share the row rendering
path, so the action menu and label filter apply to both.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: claude-code:opus-4-7 [Edit] [chrome-devtools-mcp] [Skill:critique] [Skill:audit] [Skill:polish]
* fix(react-ui/nodes): suppress slot badge on agent workers
Agent workers don't load models, so the per-node replica capacity is
inapplicable to them. Showing "1× slots" on agent rows was a tiny
inconsistency from the unified rendering path — gate the badge on
node_type !== 'agent' so it only appears on backend workers.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: claude-code:opus-4-7 [Edit] [chrome-devtools-mcp]
* refactor(react-ui/nodes): distill expanded drawer + restyle scheduling form
The expanded node drawer used to stack five panels — slot badge,
filled capacity box, Loaded Models h4+empty-state, Installed Backends
h4+empty-state, Labels h4+chips+form — making routine inspections feel
like a control panel. The scheduling rule form wrapped its mode toggle
as two 50%-width filled buttons that competed visually with the actual
primary action.
* Drawer: collapse three rarely-touched config zones (Capacity,
Backends, Labels) into one `<details>` "Manage" disclosure (closed by
default) with small uppercase eyebrow labels for each zone instead of
parallel h4 sub-headings. Loaded Models stays as the at-a-glance
headline with a single-line empty hint instead of a boxed empty state.
CapacityEditor renders flat (no filled background) — the Manage
disclosure provides framing.
* Scheduling form: replace the chunky 50%-width button-tabs with the
project's existing `.segmented` control (icon + label, sized to
content). Mode hint becomes a single tied line below. Fields stack
vertically with helper text under inputs and a hairline divider above
the right-aligned Save / Cancel.
The empty drawer collapses from ~5 stacked sections (~280px tall) to
two lines (~80px). The scheduling form now reads as a designed dialog
instead of raw building blocks. Both surfaces now match the typographic
density and weight of the rest of the admin pages.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: claude-code:opus-4-7 [Edit] [chrome-devtools-mcp] [Skill:distill] [Skill:audit] [Skill:polish]
* feat(react-ui/nodes): replace scheduling form's model picker with searchable combobox
The native <select> made operators scroll through every gallery entry to
find a model name. The project already has SearchableModelSelect (used
in Studio/Talk/etc.) which combines free-text search with the gallery
list and accepts typed model names that aren't installed yet — useful
for pre-staging a scheduling rule before the node it'll run on has
finished bootstrapping.
Also drops the now-unused useModels import (the combobox manages the
gallery hook internally).
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: claude-code:opus-4-7 [Edit]
* refactor(react-ui/nodes): consolidate key/value chip editor + add replica preset chips
The Nodes page was rendering the same key=value chip pattern in two
places with subtly different markup: the Labels editor in the expanded
drawer and (post-distill) the Node Selector input in the scheduling
form. The form's input was also a comma-separated string that operators
were getting wrong.
* Extract <KeyValueChips> as a fully controlled chip-builder. Parent
owns the map and decides what onAdd/onRemove does — form state for the
scheduling form, API calls for the live drawer Labels editor. Same
visuals everywhere; one component to change when polish needs apply.
* Replace the comma-separated Node Selector text input with KeyValueChips.
Operators were copying syntax from docs and missing commas; the chip
vocabulary makes the key=value structure self-documenting.
* Add <ReplicaInput>: numeric input + quick-pick preset chips for Min/Max
replicas. Picked over a slider because replica counts are exact specs
derived from VRAM math (operator decision, not a fuzzy estimate). The
chips give one-click access to common values (1/2/3/4 for Min,
0=no-limit/2/4/8 for Max) without the slider's special-value problem
(MaxReplicas=0 is categorical, not a position on a continuum).
* Drop the now-unused labelInputs state in the Nodes page (the inline
label editor's per-node draft state lived there and is now owned by
KeyValueChips).
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: claude-code:opus-4-7 [Edit] [Skill:distill]
* test: fix CI fallout from multi-replica refactor (e2e/distributed + playwright)
Two breakages caught by CI that didn't surface in the local run:
* tests/e2e/distributed/*.go — multiple files used the pre-PR2 registry
signatures for SetNodeModel / IncrementInFlight / DecrementInFlight /
RemoveNodeModel / TouchNodeModel / GetNodeModel / SetNodeModelLoadInfo
and one stale adapter.InstallBackend call in node_lifecycle_test.go.
All updated to pass replicaIndex=0 — these tests don't exercise
multi-replica behavior, they just need to compile against the new
signatures. The chip-builder tests in core/services/nodes/ already
cover the multi-replica logic.
* core/http/react-ui/e2e/nodes-per-node-backend-actions.spec.js — the
drawer's distill refactor moved Backends inside a "Manage" <details>
disclosure that's collapsed by default. The test helper expanded the
node row but never opened Manage, so the per-node backend table was
never in the DOM. Helper now clicks `.node-manage > summary` after
expanding the row.
All 100 playwright tests pass locally; tests/e2e/distributed compiles
clean.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: claude-code:opus-4-7 [Edit] [Bash]
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-27 19:20:05 +00:00
|
|
|
|
/* Small caps eyebrow inside the drawer's "Manage" disclosure. Replaces the
|
|
|
|
|
|
h4 sub-headings that used to stack inside the drawer — at this depth, an
|
|
|
|
|
|
eyebrow keeps the typographic hierarchy from feeling parallel to the
|
|
|
|
|
|
page-level h1/h2 stack. */
|
|
|
|
|
|
.drawer-eyebrow {
|
|
|
|
|
|
font-size: 0.6875rem;
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
letter-spacing: 0.06em;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
margin-bottom: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* "Manage" disclosure inside the node drawer. The chevron rotates with the
|
|
|
|
|
|
open state so the affordance reads as an accordion, not a link. */
|
|
|
|
|
|
.node-manage > summary {
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.node-manage > summary::-webkit-details-marker {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.node-manage > summary:focus-visible {
|
|
|
|
|
|
outline: 2px solid var(--color-primary);
|
|
|
|
|
|
outline-offset: 2px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.node-manage__chevron {
|
|
|
|
|
|
font-size: 0.625rem;
|
|
|
|
|
|
transition: transform var(--duration-fast) ease-out;
|
|
|
|
|
|
}
|
|
|
|
|
|
.node-manage[open] > summary .node-manage__chevron {
|
|
|
|
|
|
transform: rotate(90deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(distributed): sync state with frontends, better backend management reporting (#9426)
* fix(distributed): detect backend upgrades across worker nodes
Before this change `DistributedBackendManager.CheckUpgrades` delegated to the
local manager, which read backends from the frontend filesystem. In
distributed deployments the frontend has no backends installed locally —
they live on workers — so the upgrade-detection loop never ran and the UI
silently never surfaced upgrades even when the gallery advertised newer
versions or digests.
Worker-side: NATS backend.list reply now carries Version, URI and Digest
for each installed backend (read from metadata.json).
Frontend-side: DistributedBackendManager.ListBackends aggregates per-node
refs (name, status, version, digest) instead of deduping, and CheckUpgrades
feeds that aggregation into gallery.CheckUpgradesAgainst — a new entrypoint
factored out of CheckBackendUpgrades so both paths share the same core
logic.
Cluster drift policy: when per-node version/digest tuples disagree, the
backend is flagged upgradeable regardless of whether any single node
matches the gallery, and UpgradeInfo.NodeDrift enumerates the outliers so
operators can see *why* it is out of sync. The next upgrade-all realigns
the cluster.
Tests cover: drift detection, unanimous-match (no upgrade), and the
empty-installed-version path that the old distributed code silently
missed.
* feat(ui): surface backend upgrades in the System page
The System page (Manage.jsx) only showed updates as a tiny inline arrow,
so operators routinely missed them. Port the Backend Gallery's upgrade UX
so System speaks the same visual language:
- Yellow banner at the top of the Backends tab when upgrades are pending,
with an "Upgrade all" button (serial fan-out, matches the gallery) and a
"Updates only" filter toggle.
- Warning pill (↑ N) next to the tab label so the count is glanceable even
when the banner is scrolled out of view.
- Per-row labeled "Upgrade to vX.Y" button (replaces the icon-only button
that silently flipped semantics between Reinstall and Upgrade), plus an
"Update available" badge in the new Version column.
- New columns: Version (with upgrade + drift chips), Nodes (per-node
attribution badges for distributed mode, degrading to a compact
"on N nodes · M offline" chip above three nodes), Installed (relative
time).
- System backends render a "Protected" chip instead of a bare "—" so rows
still align and the reason is obvious.
- Delete uses the softer btn-danger-ghost so rows don't scream red; the
ConfirmDialog still owns the "are you sure".
The upgrade checker also needed the same per-worker fix as the previous
commit: NewUpgradeChecker now takes a BackendManager getter so its
periodic runs call the distributed CheckUpgrades (which asks workers)
instead of the empty frontend filesystem. Without this the /api/backends/
upgrades endpoint stayed empty in distributed mode even with the protocol
change in place.
New CSS primitives — .upgrade-banner, .tab-pill, .badge-row, .cell-stack,
.cell-mono, .cell-muted, .row-actions, .btn-danger-ghost — all live in
App.css so other pages can adopt them without duplicating styles.
* feat(ui): polish the Nodes page so it reads like a product
The Nodes page was the biggest visual liability in distributed mode.
Rework the main dashboard surfaces in place without changing behavior:
StatCards: uniform height (96px min), left accent bar colored by the
metric's semantic (success/warning/error/primary), icon lives in a
36x36 soft-tinted chip top-right, value is left-aligned and large.
Grid auto-fills so the row doesn't collapse on narrow viewports. This
replaces the previous thin-bordered boxes with inconsistent heights.
Table rows: expandable rows now show a chevron cue on the left (rotates
on expand) so users know rows open. Status cell became a dedicated chip
with an LED-style halo dot instead of a bare bullet. Action buttons gained
labels — "Approve", "Resume", "Drain" — so the icons aren't doing all
the semantic work; the destructive remove action uses the softer
btn-danger-ghost variant so rows don't scream red, with the ConfirmDialog
still owning the real "are you sure". Applied cell-mono/cell-muted
utility classes so label chips and addresses share one spacing/font
grammar instead of re-declaring inline styles everywhere.
Expanded drawer: empty states for Loaded Models and Installed Backends
now render as a proper drawer-empty card (dashed border, icon, one-line
hint) instead of a plain muted string that read like broken formatting.
Tabs: three inline-styled buttons became the shared .tab class so they
inherit focus ring, hover state, and the rest of the design system —
matches the System page.
"Add more workers" toggle turned into a .nodes-add-worker dashed-border
button labelled "Register a new worker" (action voice) instead of a
chevron + muted link that operators kept mistaking for broken text.
New shared CSS primitives carry over to other pages:
.stat-grid + .stat-card, .row-chevron, .node-status, .drawer-empty,
.nodes-add-worker.
* feat(distributed): durable backend fan-out + state reconciliation
Two connected problems handled together:
1) Backend delete/install/upgrade used to silently skip non-healthy nodes,
so a delete during an outage left a zombie on the offline node once it
returned. The fan-out now records intent in a new pending_backend_ops
table before attempting the NATS round-trip. Currently-healthy nodes
get an immediate attempt; everyone else is queued. Unique index on
(node_id, backend, op) means reissuing the same operation refreshes
next_retry_at instead of stacking duplicates.
2) Loaded-model state could drift from reality: a worker OOM'd, got
killed, or restarted a backend process would leave a node_models row
claiming the model was still loaded, feeding ghost entries into the
/api/nodes/models listing and the router's scheduling decisions.
The existing ReplicaReconciler gains two new passes that run under a
fresh KeyStateReconciler advisory lock (non-blocking, so one wedged
frontend doesn't freeze the cluster):
- drainPendingBackendOps: retries queued ops whose next_retry_at has
passed on currently-healthy nodes. Success deletes the row; failure
bumps attempts and pushes next_retry_at out with exponential backoff
(30s → 15m cap). ErrNoResponders also marks the node unhealthy.
- probeLoadedModels: gRPC-HealthChecks addresses the DB thinks are
loaded but hasn't seen touched in the last probeStaleAfter (2m).
Unreachable addresses are removed from the registry. A pluggable
ModelProber lets tests substitute a fake without standing up gRPC.
DistributedBackendManager exposes DeleteBackendDetailed so the HTTP
handler can surface per-node outcomes ("2 succeeded, 1 queued") to the
UI in a follow-up commit; the existing DeleteBackend still returns
error-only for callers that don't care about node breakdown.
Multi-frontend safety: the state pass uses advisorylock.TryWithLockCtx
on a new key so N frontends coordinate — the same pattern the health
monitor and replica reconciler already rely on. Single-node mode runs
both passes inline (adapter is nil, state drain is a no-op).
Tests cover the upsert semantics, backoff math, the probe removing an
unreachable model but keeping a reachable one, and filtering by
probeStaleAfter.
* feat(ui): show cluster distribution of models in the System page
When a frontend restarted in distributed mode, models that workers had
already loaded weren't visible until the operator clicked into each node
manually — the /api/models/capabilities endpoint only knew about
configs on the frontend's filesystem, not the registry-backed truth.
/api/models/capabilities now joins in ListAllLoadedModels() when the
registry is active, returning loaded_on[] with node id/name/state/status
for each model. Models that live in the registry but lack a local config
(the actual ghosts, not recovered from the frontend's file cache) still
surface with source="registry-only" so operators can see and persist
them; without that emission they'd be invisible to this frontend.
Manage → Models replaces the old Running/Idle pill with a distribution
cell that lists the first three nodes the model is loaded on as chips
colored by state (green loaded, blue loading, amber anything else). On
wider clusters the remaining count collapses into a +N chip with a
title-attribute breakdown. Disabled / single-node behavior unchanged.
Adopted models get an extra "Adopted" ghost-icon chip with hover copy
explaining what it means and how to make it permanent.
Distributed mode also enables a 10s auto-refresh and a "Last synced Xs
ago" indicator next to the Update button so ghost rows drop off within
one reconcile tick after their owning process dies. Non-distributed
mode is untouched — no polling, no cell-stack, same old Running/Idle.
* feat(ui): NodeDistributionChip — shared per-node attribution component
Large clusters were going to break the Manage → Backends Nodes column:
the old inline logic rendered every node as a badge and would shred the
layout at >10 workers, plus the Manage → Models distribution cell had
copy-pasted its own slightly-different version.
NodeDistributionChip handles any cluster size with two render modes:
- small (≤3 nodes): inline chips of node names, colored by health.
- large: a single "on N nodes · M offline · K drift" summary chip;
clicking opens a Popover with a per-node table (name, status,
version, digest for backends; name, status, state for models).
Drift counting mirrors the backend's summarizeNodeDrift so the UI
number matches UpgradeInfo.NodeDrift. Digests are truncated to the
docker-style 12-char form with the full value preserved in the title.
Popover is a new general-purpose primitive: fixed positioning anchored
to the trigger, flips above when there's no room below, closes on
outside-click or Escape, returns focus to the trigger. Uses .card as
its surface so theming is inherited. Also useful for a future
labels-editor popup and the user menu.
Manage.jsx drops its duplicated inline Nodes-column + loaded_on cell
and uses the shared chip with context="backends" / "models"
respectively. Delete code removes ~40 lines of ad-hoc logic.
* feat(ui): shared FilterBar across the System page tabs
The Backends gallery had a nice search + chip + toggle strip; the System
page had nothing, so the two surfaces felt like different apps. Lift the
pattern into a reusable FilterBar and wire both System tabs through it.
New component core/http/react-ui/src/components/FilterBar.jsx renders a
search input, a role="tablist" chip row (aria-selected for a11y), and
optional toggles / right slot. Chips support an optional `count` which
the System page uses to show "User 3", "Updates 1" etc.
System Models tab: search by id or backend; chips for
All/Running/Idle/Disabled/Pinned plus a conditional Distributed chip in
distributed mode. "Last synced" + Update button live in the right slot.
System Backends tab: search by name/alias/meta-backend-for; chips for
All/User/System/Meta plus conditional Updates / Offline-nodes chips
when relevant. The old ad-hoc "Updates only" toggle from the upgrade
banner folded into the Updates chip — one source of truth for that
filter. Offline chip only appears in distributed mode when at least
one backend has an unhealthy node, so the chip row stays quiet on
healthy clusters.
Filter state persists in URL query params (mq/mf/bq/bf) so deep links
and tab switches keep the operator's filter context instead of
resetting every time.
Also adds an "Adopted" distribution path: when a model in
/api/models/capabilities carries source="registry-only" (discovered on
a worker but not configured locally), the Models tab shows a ghost chip
labelled "Adopted" with hover copy explaining how to persist it — this
is what closes the loop on the ghost-model story end-to-end.
2026-04-19 15:55:53 +00:00
|
|
|
|
/* Node-status indicator — replaces the tiny bullet with a proper LED-style
|
|
|
|
|
|
dot next to a bold status label. Colors are applied inline from statusConfig
|
|
|
|
|
|
so one primitive handles healthy/unhealthy/draining/pending in one shape. */
|
|
|
|
|
|
.node-status {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
}
|
|
|
|
|
|
.node-status__dot {
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
box-shadow: 0 0 0 3px color-mix(in srgb, currentColor 15%, transparent);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Row-chevron cell — small 20px toggle used in table rows that expand.
|
|
|
|
|
|
The row itself is still clickable; the chevron provides the visible
|
|
|
|
|
|
affordance users were missing. */
|
|
|
|
|
|
.row-chevron {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 20px;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
transition: transform var(--duration-fast) var(--ease-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
.row-chevron.is-expanded {
|
|
|
|
|
|
transform: rotate(90deg);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Upgrade banner — the yellow strip operators see when updates are available.
|
|
|
|
|
|
Mirrors the gallery so both pages speak the same visual language. */
|
|
|
|
|
|
.upgrade-banner {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
background: var(--color-warning-light);
|
|
|
|
|
|
border: 1px solid var(--color-warning);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
color: var(--color-warning);
|
|
|
|
|
|
}
|
|
|
|
|
|
.upgrade-banner__text {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.upgrade-banner__actions {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
2026-03-05 20:47:12 +00:00
|
|
|
|
|
|
|
|
|
|
/* Tabs */
|
|
|
|
|
|
.tabs {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 0;
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab {
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border-bottom: 2px solid transparent;
|
|
|
|
|
|
transition: all var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab:hover {
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.tab-active {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
border-bottom-color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Tables */
|
|
|
|
|
|
.table-container {
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border-collapse: collapse;
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table th {
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table td {
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-divider);
|
|
|
|
|
|
color: var(--color-text-primary);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
transition: background var(--duration-fast) var(--ease-default);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table tr:last-child td {
|
|
|
|
|
|
border-bottom: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.table tr:hover td {
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Toggle switch */
|
|
|
|
|
|
.toggle {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
width: 36px;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toggle input {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
width: 0;
|
|
|
|
|
|
height: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toggle-slider {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
background: var(--color-toggle-off, #CBD5E1);
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
transition: background var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toggle-slider::before {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
height: 14px;
|
|
|
|
|
|
width: 14px;
|
|
|
|
|
|
left: 2px;
|
|
|
|
|
|
bottom: 2px;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
transition: transform var(--duration-fast);
|
|
|
|
|
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toggle input:checked + .toggle-slider {
|
|
|
|
|
|
background: var(--color-primary);
|
|
|
|
|
|
border-color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.toggle input:checked + .toggle-slider::before {
|
|
|
|
|
|
transform: translateX(16px);
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 20:40:51 +00:00
|
|
|
|
/* Model checkbox list */
|
|
|
|
|
|
.model-list {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
max-height: 200px;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
padding: var(--spacing-xs);
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-list::-webkit-scrollbar {
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-list::-webkit-scrollbar-track {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-list::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background: var(--color-border-default);
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: 6px var(--spacing-sm);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
transition: background var(--duration-fast) var(--ease-default);
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-item:hover {
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-item.model-item-checked {
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-item input[type="checkbox"] {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-item-check {
|
|
|
|
|
|
width: 18px;
|
|
|
|
|
|
height: 18px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
border: 2px solid var(--color-border-default);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
transition: all var(--duration-fast) var(--ease-default);
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-item:hover .model-item-check {
|
|
|
|
|
|
border-color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-item-checked .model-item-check {
|
|
|
|
|
|
background: var(--color-primary);
|
|
|
|
|
|
border-color: var(--color-primary);
|
|
|
|
|
|
box-shadow: 0 0 0 1px var(--color-primary-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-item-checked .model-item-check i {
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
animation: checkPop var(--duration-fast) var(--ease-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes checkPop {
|
|
|
|
|
|
0% { transform: scale(0); }
|
|
|
|
|
|
60% { transform: scale(1.2); }
|
|
|
|
|
|
100% { transform: scale(1); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-item-name {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
font-family: var(--font-mono);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.model-item-checked .model-item-name {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
/* Collapsible */
|
|
|
|
|
|
.collapsible-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: var(--spacing-sm) 0;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.collapsible-header i {
|
|
|
|
|
|
transition: transform var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.collapsible-header.open i {
|
|
|
|
|
|
transform: rotate(90deg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Search bar */
|
|
|
|
|
|
.search-bar {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-bar .search-icon {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: var(--spacing-sm);
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.search-bar .input {
|
|
|
|
|
|
padding-left: 2rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Pagination */
|
|
|
|
|
|
.pagination {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
margin-top: var(--spacing-lg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pagination-btn {
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
transition: all var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pagination-btn:hover:not(:disabled) {
|
|
|
|
|
|
border-color: var(--color-primary-border);
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pagination-btn.active {
|
|
|
|
|
|
background: var(--color-primary);
|
|
|
|
|
|
color: var(--color-primary-text);
|
|
|
|
|
|
border-color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pagination-btn:disabled {
|
|
|
|
|
|
opacity: 0.4;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Filter buttons */
|
|
|
|
|
|
.filter-bar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.filter-btn {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
padding: 6px var(--spacing-md);
|
|
|
|
|
|
background: var(--color-surface-raised);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
color: var(--color-text-secondary);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
border: 1px solid var(--color-border-default);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
cursor: pointer;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: var(--font-weight-medium);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
font-family: inherit;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
box-shadow: var(--shadow-subtle);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
transition: all var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.filter-btn:hover {
|
|
|
|
|
|
border-color: var(--color-primary-border);
|
|
|
|
|
|
color: var(--color-primary);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
|
box-shadow: var(--shadow-sm);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.filter-btn.active {
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
border-color: var(--color-primary-border);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 20:40:51 +00:00
|
|
|
|
/* Login page */
|
|
|
|
|
|
.login-page {
|
|
|
|
|
|
min-height: 100vh;
|
|
|
|
|
|
min-height: 100dvh;
|
|
|
|
|
|
background: var(--color-bg-primary);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
padding: var(--spacing-xl);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-card {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 400px;
|
|
|
|
|
|
padding: var(--spacing-xl);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-header {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin-bottom: var(--spacing-xl);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-logo {
|
|
|
|
|
|
width: 56px;
|
|
|
|
|
|
height: 56px;
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-title {
|
|
|
|
|
|
font-size: 1.5rem;
|
|
|
|
|
|
font-weight: 700;
|
|
|
|
|
|
margin-bottom: var(--spacing-xs);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(branding): admin-configurable instance name, tagline, and assets (#9635)
Adds a whitelabeling feature so an operator can replace the LocalAI
instance name, tagline, square logo, horizontal logo, and favicon from
the admin Settings page. Defaults fall back to the bundled assets so
existing installs are unaffected.
The public GET /api/branding endpoint is reachable pre-auth so the
login screen can render the configured branding before sign-in.
Mutating routes (POST/DELETE /api/branding/asset/:kind) remain
admin-only. Text fields (instance_name, instance_tagline) ride the
existing /api/settings flow; binary assets get a dedicated multipart
upload route that persists files under DynamicConfigsDir/branding/.
To prevent the Settings page's stale local state from clobbering an
upload on save, UpdateSettingsEndpoint preserves whatever the on-disk
asset filename fields are regardless of the body — /api/branding/asset/*
are the sole writers for those fields.
The MCP catalog gains get_branding and set_branding tools (text fields
only; file upload stays UI-only) plus a configure_branding skill prompt.
While wiring this up, the same restart-loss class of bug surfaced for
several existing fields whose RuntimeSettings entries were never read
by the startup loader. Fix loadRuntimeSettingsFromFile() to load:
- branding (instance_name, instance_tagline, *_file basenames)
- auto_upgrade_backends, prefer_development_backends
- localai_assistant_enabled
- open_responses_store_ttl
- the 7 existing AgentPool fields (enabled, default/embedding model,
chunking sizes, enable_logs, collection_db_path)
Also exposes 3 new AgentPool runtime settings (vector_engine,
database_url, agent_hub_url) via /api/settings + the Settings UI, with
the same load-on-startup wiring. The file watcher's manual-edit path
is intentionally not changed — the in-process API endpoints already
update appConfig directly, so the watcher is redundant for supported
flows and a separate refactor for everything else.
15 TDD specs cover the loader behaviour (1 branding + 11 adjacent + 3
new agent-pool); 2 specs cover the persistence helpers and the
clobber-prevention contract.
Assisted-by: claude-code:claude-opus-4-7
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-05-02 13:51:36 +00:00
|
|
|
|
.login-tagline {
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
font-size: 0.9375rem;
|
|
|
|
|
|
margin-bottom: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 20:40:51 +00:00
|
|
|
|
.login-subtitle {
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-alert {
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-alert-error {
|
|
|
|
|
|
background: var(--color-error-light);
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
border: 1px solid var(--color-error-border);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-alert-success {
|
|
|
|
|
|
background: var(--color-success-light);
|
|
|
|
|
|
color: var(--color-success);
|
|
|
|
|
|
border: 1px solid var(--color-success-border);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-divider {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
margin: var(--spacing-lg) 0;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-divider::before,
|
|
|
|
|
|
.login-divider::after {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
height: 1px;
|
|
|
|
|
|
background: var(--color-border-subtle);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-footer {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin-top: var(--spacing-md);
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-link {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-link:hover {
|
|
|
|
|
|
color: var(--color-primary-hover);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-token-toggle {
|
|
|
|
|
|
margin-top: var(--spacing-lg);
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-token-toggle > button {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-token-toggle > button:hover {
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.login-token-form {
|
|
|
|
|
|
margin-top: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
/* Empty state */
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
/* Empty state — editorial: eyebrow rule + large mono headline + lede.
|
|
|
|
|
|
Existing pages pass `icon`, `title`, `text` children into .empty-state as
|
|
|
|
|
|
nested elements; the rules below re-style each without JSX changes. */
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.empty-state {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
padding: var(--spacing-3xl) var(--spacing-xl);
|
|
|
|
|
|
max-width: 640px;
|
|
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
animation: fadeIn var(--duration-slow) var(--ease-spring);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
/* Center the column within its parent without shrinking type. */
|
|
|
|
|
|
.empty-state > * { max-width: 100%; }
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.empty-state-icon {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
font-size: 1.5rem;
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
|
margin: 0;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-state-title {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
font-size: clamp(1.5rem, 3vw, var(--text-3xl));
|
|
|
|
|
|
font-weight: var(--font-weight-regular);
|
|
|
|
|
|
letter-spacing: -0.03em;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
line-height: 1.15;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.empty-state-text {
|
|
|
|
|
|
color: var(--color-text-secondary);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
font-size: var(--text-base);
|
|
|
|
|
|
line-height: var(--leading-normal);
|
|
|
|
|
|
max-width: 52ch;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Opt-in editorial sub-parts — pages can adopt these class names over time */
|
|
|
|
|
|
.empty-state__eyebrow {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
font-size: 0.625rem;
|
|
|
|
|
|
letter-spacing: 0.24em;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
.empty-state__eyebrow::before {
|
|
|
|
|
|
content: "";
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
width: 24px;
|
|
|
|
|
|
height: 1px;
|
|
|
|
|
|
background: var(--color-border-strong);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Animations */
|
|
|
|
|
|
@keyframes spin {
|
|
|
|
|
|
to { transform: rotate(360deg); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes slideIn {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
from { opacity: 0; transform: translateX(12px); }
|
|
|
|
|
|
to { opacity: 1; transform: translateX(0); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes toastSlideIn {
|
|
|
|
|
|
from { opacity: 0; transform: translateX(12px); }
|
|
|
|
|
|
to { opacity: 1; transform: translateX(0); }
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes fadeIn {
|
|
|
|
|
|
from { opacity: 0; }
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
to { opacity: 1; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes popIn {
|
|
|
|
|
|
from { opacity: 0; transform: scale(0.96) translateY(4px); }
|
|
|
|
|
|
to { opacity: 1; transform: scale(1) translateY(0); }
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes pulse {
|
|
|
|
|
|
0%, 100% { opacity: 1; }
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
50% { opacity: 0.5; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes attentionPulse {
|
|
|
|
|
|
0%, 100% { opacity: 0.65; }
|
|
|
|
|
|
50% { opacity: 1; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes opsBarBreathe {
|
|
|
|
|
|
0%, 100% { opacity: 0.85; }
|
|
|
|
|
|
50% { opacity: 1; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes breathingRing {
|
|
|
|
|
|
0%, 100% { box-shadow: inset 0 0 0 1.5px var(--color-primary-light); }
|
|
|
|
|
|
50% { box-shadow: inset 0 0 0 2px var(--color-primary); }
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 20:40:51 +00:00
|
|
|
|
@keyframes messageSlideIn {
|
|
|
|
|
|
from { opacity: 0; transform: translateY(8px); }
|
|
|
|
|
|
to { opacity: 1; transform: translateY(0); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@keyframes dropdownIn {
|
|
|
|
|
|
from { opacity: 0; transform: translateY(-4px); }
|
|
|
|
|
|
to { opacity: 1; transform: translateY(0); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Page route transitions */
|
|
|
|
|
|
.page-transition {
|
|
|
|
|
|
animation: fadeIn 200ms ease;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
display: flex;
|
2026-03-19 20:40:51 +00:00
|
|
|
|
flex-direction: column;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 0;
|
2026-03-09 22:42:47 +00:00
|
|
|
|
min-width: 0;
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Chat-specific styles */
|
|
|
|
|
|
.chat-layout {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
position: relative;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-sidebar {
|
|
|
|
|
|
width: 260px;
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
border-right: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
transition: width 200ms ease, opacity 150ms ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-sidebar.hidden {
|
|
|
|
|
|
width: 0;
|
|
|
|
|
|
border-right: none;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-sidebar-header {
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-list {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
padding: var(--spacing-xs);
|
|
|
|
|
|
scrollbar-width: thin;
|
|
|
|
|
|
scrollbar-color: var(--color-border-subtle) transparent;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-list::-webkit-scrollbar { width: 4px; }
|
|
|
|
|
|
.chat-list::-webkit-scrollbar-track { background: transparent; }
|
|
|
|
|
|
.chat-list::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background: var(--color-border-subtle);
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-list-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
cursor: pointer;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-sm);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
color: var(--color-text-secondary);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
border-left: 3px solid transparent;
|
|
|
|
|
|
transition: background var(--duration-fast), color var(--duration-fast), border-color var(--duration-fast);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 21:53:03 +00:00
|
|
|
|
.chat-list-item:hover:not(.active) {
|
|
|
|
|
|
background: var(--color-surface-hover);
|
|
|
|
|
|
color: var(--color-text-primary);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-list-item.active {
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
color: var(--color-primary);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
border-left-color: var(--color-primary);
|
|
|
|
|
|
font-weight: var(--font-weight-medium);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-list-item-info {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-list-item-top {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-list-item-name {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-list-item-time {
|
|
|
|
|
|
font-size: 0.625rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-list-item-preview {
|
|
|
|
|
|
font-size: 0.6875rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-list-item-delete {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 2px;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-list-item:hover .chat-list-item-delete {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-main {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
position: relative;
|
2026-03-09 22:42:47 +00:00
|
|
|
|
overflow: hidden;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-messages {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow-y: auto;
|
2026-03-09 22:42:47 +00:00
|
|
|
|
overflow-x: hidden;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
gap: var(--spacing-sm);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
scrollbar-width: thin;
|
|
|
|
|
|
scrollbar-color: var(--color-border-subtle) transparent;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
transition: padding-top var(--duration-normal) var(--ease-default);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
.chat-messages::-webkit-scrollbar { width: 6px; }
|
|
|
|
|
|
.chat-messages::-webkit-scrollbar-track { background: transparent; }
|
|
|
|
|
|
.chat-messages::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background: var(--color-border-subtle);
|
|
|
|
|
|
border-radius: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-messages::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
|
background: var(--color-border-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-message {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
max-width: 88%;
|
2026-03-09 22:42:47 +00:00
|
|
|
|
min-width: 0;
|
2026-03-19 20:40:51 +00:00
|
|
|
|
animation: messageSlideIn 250ms ease-out;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-message-user {
|
|
|
|
|
|
align-self: flex-end;
|
|
|
|
|
|
flex-direction: row-reverse;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-message-assistant {
|
|
|
|
|
|
align-self: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-message-avatar {
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
width: 28px;
|
|
|
|
|
|
height: 28px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
font-size: 0.75rem;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-message-user .chat-message-avatar {
|
|
|
|
|
|
background: var(--color-primary);
|
2026-04-18 23:01:01 +00:00
|
|
|
|
color: var(--color-primary-text);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
/* Assistant gets the left-border accent on the bubble; the avatar is
|
|
|
|
|
|
visual noise once that accent is in place. */
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.chat-message-assistant .chat-message-avatar {
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
display: none;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-message-bubble {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-message-model {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
font-family: var(--font-mono);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
font-size: 0.625rem;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
font-weight: 500;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
text-transform: uppercase;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
letter-spacing: 0.14em;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
padding-left: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-message-content {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-left: 2px solid var(--color-border-strong);
|
|
|
|
|
|
border-radius: 0;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
padding: 0 var(--spacing-md);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
font-size: var(--text-base);
|
|
|
|
|
|
line-height: var(--leading-normal);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
word-break: break-word;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
color: var(--color-text-primary);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-message-user .chat-message-content {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
border: 1px solid var(--color-primary-border);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border-radius: 16px 4px 16px 16px;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-message-content pre {
|
|
|
|
|
|
background: var(--color-bg-primary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
padding: var(--spacing-sm);
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
margin: var(--spacing-sm) 0;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-message-user .chat-message-content pre {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
border-color: var(--color-border-subtle);
|
|
|
|
|
|
color: var(--color-text-primary);
|
2026-04-18 23:01:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
.chat-message-user .chat-message-content code {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
color: var(--color-text-primary);
|
2026-04-18 23:01:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
.chat-message-user .chat-message-content a {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
color: var(--color-primary);
|
2026-04-18 23:01:01 +00:00
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
text-underline-offset: 2px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-message-content code {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
font-family: var(--font-mono);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-message-content p {
|
|
|
|
|
|
margin: var(--spacing-xs) 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-message-content p:first-child {
|
|
|
|
|
|
margin-top: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-message-content p:last-child {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Message action buttons */
|
|
|
|
|
|
.chat-message-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transition: opacity 150ms;
|
|
|
|
|
|
padding-left: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-message:hover .chat-message-actions {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-message-actions button {
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 3px 6px;
|
|
|
|
|
|
font-size: 0.6875rem;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
transition: all 150ms;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-message-actions button:hover {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
border-color: var(--color-primary-border);
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 17:28:44 +00:00
|
|
|
|
.chat-message-system {
|
|
|
|
|
|
align-self: center;
|
|
|
|
|
|
max-width: 90%;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-message-system .chat-message-bubble {
|
2026-03-09 22:42:47 +00:00
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
letter-spacing: 0.01em;
|
|
|
|
|
|
padding: 2px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-message-system .chat-message-content {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 0;
|
|
|
|
|
|
padding: 2px var(--spacing-sm);
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
color: var(--color-text-muted);
|
2026-03-09 17:28:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
.chat-message-timestamp {
|
|
|
|
|
|
font-size: 0.6875rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.chat-input-area {
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
padding: var(--spacing-xs) var(--spacing-md) var(--spacing-sm);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
border-top: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-input-wrapper {
|
|
|
|
|
|
display: flex;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
flex-wrap: wrap;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
background: var(--color-surface-raised);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border: 1px solid var(--color-border-default);
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
border-radius: var(--radius-md);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
padding: var(--spacing-xs);
|
|
|
|
|
|
transition: border-color var(--duration-fast), box-shadow var(--duration-fast);
|
|
|
|
|
|
}
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
|
|
|
|
|
|
/* Mode chips (Canvas, MCP) — render at the start of the input wrapper so
|
|
|
|
|
|
the user sees what's armed for the next message. Compact, low-noise
|
|
|
|
|
|
when off; subtly highlighted when on. The MCP trigger inherits the
|
|
|
|
|
|
same look via the nested-selector overrides below. */
|
|
|
|
|
|
.chat-input-modes {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mode-chip {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
height: 28px;
|
|
|
|
|
|
padding: 0 10px;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background var(--duration-fast), border-color var(--duration-fast), color var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mode-chip:hover {
|
|
|
|
|
|
background: var(--color-bg-hover);
|
|
|
|
|
|
border-color: var(--color-border-default);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mode-chip:focus-visible {
|
|
|
|
|
|
outline: 2px solid var(--color-focus-ring);
|
|
|
|
|
|
outline-offset: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mode-chip-on,
|
|
|
|
|
|
.chat-mode-chip-on:hover {
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
border-color: var(--color-primary-border);
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mode-chip i {
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mode-chip-label {
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mode-chip-count {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
min-width: 18px;
|
|
|
|
|
|
height: 16px;
|
|
|
|
|
|
padding: 0 5px;
|
|
|
|
|
|
margin-left: 2px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: var(--color-primary);
|
|
|
|
|
|
color: var(--color-primary-text);
|
|
|
|
|
|
font-size: 0.65rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mode-chip-count:hover {
|
|
|
|
|
|
filter: brightness(1.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* MCP popover sits above the chip when it's anchored to the input row
|
|
|
|
|
|
(otherwise it would drop off-screen). */
|
|
|
|
|
|
.chat-input-modes .chat-mcp-dropdown-menu {
|
|
|
|
|
|
top: auto;
|
|
|
|
|
|
bottom: calc(100% + 4px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* When the MCP dropdown lives inside the modes row, make its trigger
|
|
|
|
|
|
match the chip aesthetic without altering the component itself. */
|
|
|
|
|
|
.chat-input-modes .chat-mcp-dropdown > .btn {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
height: 28px;
|
|
|
|
|
|
padding: 0 10px;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
box-shadow: none;
|
|
|
|
|
|
text-transform: none;
|
|
|
|
|
|
letter-spacing: 0;
|
|
|
|
|
|
transition: background var(--duration-fast), border-color var(--duration-fast), color var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-input-modes .chat-mcp-dropdown > .btn:hover {
|
|
|
|
|
|
background: var(--color-bg-hover);
|
|
|
|
|
|
border-color: var(--color-border-default);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-input-modes .chat-mcp-dropdown > .btn .chat-mcp-badge {
|
|
|
|
|
|
background: var(--color-primary);
|
|
|
|
|
|
color: var(--color-primary-text);
|
|
|
|
|
|
}
|
|
|
|
|
|
/* Active state when at least one MCP source is selected — UnifiedMCPDropdown
|
|
|
|
|
|
exposes its count via .chat-mcp-badge, so we use :has() to lift the
|
|
|
|
|
|
chip into the "on" look. Falls back to the neutral chip look on browsers
|
|
|
|
|
|
without :has(). */
|
|
|
|
|
|
.chat-input-modes .chat-mcp-dropdown > .btn:has(.chat-mcp-badge),
|
|
|
|
|
|
.chat-input-modes .chat-mcp-dropdown > .btn:has(.chat-mcp-badge):hover {
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
border-color: var(--color-primary-border);
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 480px) {
|
|
|
|
|
|
.chat-mode-chip-label { display: none; }
|
|
|
|
|
|
.chat-mode-chip,
|
|
|
|
|
|
.chat-input-modes .chat-mcp-dropdown > .btn {
|
|
|
|
|
|
padding: 0 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.chat-input-wrapper:focus-within {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
border-color: var(--color-primary);
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
box-shadow: 0 0 0 1px var(--color-primary-border);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-attach-btn {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
border: none !important;
|
|
|
|
|
|
background: transparent !important;
|
|
|
|
|
|
color: var(--color-text-muted) !important;
|
|
|
|
|
|
padding: var(--spacing-xs) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-attach-btn:hover {
|
|
|
|
|
|
color: var(--color-primary) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-input {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
border: none;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
padding: 6px var(--spacing-sm);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
resize: none;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
min-height: 32px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
max-height: 200px;
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
overflow-y: auto;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
/* Modern auto-grow — JS auto-grow stays as a fallback for older engines. */
|
|
|
|
|
|
field-sizing: content;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-input::placeholder {
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-send-btn {
|
|
|
|
|
|
padding: var(--spacing-xs);
|
|
|
|
|
|
background: var(--color-primary);
|
2026-04-18 23:01:01 +00:00
|
|
|
|
color: var(--color-primary-text);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
flex-shrink: 0;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
align-self: flex-start;
|
2026-04-18 23:01:01 +00:00
|
|
|
|
transition: background var(--duration-fast), transform var(--duration-fast);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-send-btn:hover:not(:disabled) {
|
|
|
|
|
|
background: var(--color-primary-hover);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-send-btn:disabled {
|
|
|
|
|
|
opacity: 0.3;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
2026-03-19 20:40:51 +00:00
|
|
|
|
.chat-send-btn:active:not(:disabled) {
|
|
|
|
|
|
transform: scale(0.92);
|
|
|
|
|
|
}
|
2026-03-05 20:47:12 +00:00
|
|
|
|
|
|
|
|
|
|
.chat-stop-btn {
|
|
|
|
|
|
padding: var(--spacing-xs);
|
|
|
|
|
|
background: var(--color-error);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
flex-shrink: 0;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
align-self: flex-start;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
transition: background var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-stop-btn:hover {
|
|
|
|
|
|
background: var(--color-error-hover, #dc2626);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-token-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-lg);
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-streaming-cursor::after {
|
|
|
|
|
|
content: '';
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
height: 14px;
|
|
|
|
|
|
background: var(--color-primary);
|
|
|
|
|
|
margin-left: 2px;
|
|
|
|
|
|
animation: pulse 1s infinite;
|
|
|
|
|
|
vertical-align: text-bottom;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 20:40:51 +00:00
|
|
|
|
/* Inline streaming speed indicator */
|
|
|
|
|
|
.chat-streaming-speed {
|
|
|
|
|
|
font-size: 0.6875rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
padding-top: var(--spacing-xs);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
font-family: var(--font-mono);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Thinking dots animation */
|
|
|
|
|
|
.chat-thinking-indicator {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
min-height: 24px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-thinking-dots {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-thinking-dots span {
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
height: 6px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: var(--color-text-muted);
|
|
|
|
|
|
animation: thinkingBounce 1.4s infinite ease-in-out both;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-thinking-dots span:nth-child(1) { animation-delay: -0.32s; }
|
|
|
|
|
|
.chat-thinking-dots span:nth-child(2) { animation-delay: -0.16s; }
|
|
|
|
|
|
.chat-thinking-dots span:nth-child(3) { animation-delay: 0s; }
|
|
|
|
|
|
@keyframes thinkingBounce {
|
|
|
|
|
|
0%, 80%, 100% { transform: scale(0.6); opacity: 0.4; }
|
|
|
|
|
|
40% { transform: scale(1); opacity: 1; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-08 12:33:58 +00:00
|
|
|
|
/* Staging progress indicator (replaces thinking dots during model transfer) */
|
|
|
|
|
|
.chat-staging-progress {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
min-width: 200px;
|
|
|
|
|
|
max-width: 320px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-staging-label {
|
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-staging-label i {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-staging-detail {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-staging-bar-container {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
height: 4px;
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-staging-bar {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: var(--color-primary);
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
transition: width 300ms ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-staging-pct {
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
min-width: 32px;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-staging-file {
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-18 23:01:01 +00:00
|
|
|
|
/* Message completion flash — briefly highlights the last assistant bubble when streaming ends */
|
|
|
|
|
|
@keyframes messageCompletionFlash {
|
|
|
|
|
|
0% { box-shadow: 0 0 0 0 var(--color-primary-border); }
|
|
|
|
|
|
40% { box-shadow: 0 0 0 3px var(--color-primary-light); }
|
|
|
|
|
|
100% { box-shadow: 0 0 0 0 transparent; }
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-message-new .chat-message-content {
|
|
|
|
|
|
animation: messageCompletionFlash 600ms ease-out;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
|
.chat-message { animation: none; }
|
|
|
|
|
|
.chat-message-new .chat-message-content { animation: none; }
|
|
|
|
|
|
.chat-thinking-dots span { animation: none; opacity: 0.7; }
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
/* Chat empty state */
|
|
|
|
|
|
.chat-empty-state {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
flex: 1;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
padding: var(--spacing-lg) var(--spacing-md);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
text-align: center;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
min-height: 240px;
|
|
|
|
|
|
gap: var(--spacing-md);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
.chat-empty-state > * { margin-top: 0; margin-bottom: 0; }
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.chat-empty-icon {
|
|
|
|
|
|
font-size: 3rem;
|
|
|
|
|
|
color: var(--color-border-default);
|
|
|
|
|
|
margin-bottom: var(--spacing-lg);
|
|
|
|
|
|
width: 80px;
|
|
|
|
|
|
height: 80px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-empty-title {
|
|
|
|
|
|
font-size: 1.25rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
margin: 0 0 var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-empty-text {
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
margin: 0 0 var(--spacing-lg);
|
|
|
|
|
|
max-width: 400px;
|
|
|
|
|
|
}
|
2026-03-19 20:40:51 +00:00
|
|
|
|
.chat-empty-suggestions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
gap: var(--spacing-sm);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
justify-content: center;
|
|
|
|
|
|
margin-bottom: var(--spacing-lg);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
max-width: 520px;
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.chat-empty-suggestion {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
padding: 8px var(--spacing-md);
|
|
|
|
|
|
background: var(--color-surface-raised);
|
|
|
|
|
|
border: 1px solid var(--color-border-default);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
border-radius: var(--radius-full);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
font-weight: var(--font-weight-medium);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
cursor: pointer;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
box-shadow: var(--shadow-subtle), var(--shadow-inset-top);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
transition: all var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-empty-suggestion:hover {
|
|
|
|
|
|
border-color: var(--color-primary-border);
|
|
|
|
|
|
color: var(--color-primary);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
background: var(--color-surface-raised);
|
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
|
box-shadow: var(--shadow-sm), var(--shadow-inset-top);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.chat-empty-hints {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-empty-hints span {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-empty-hints i {
|
|
|
|
|
|
font-size: 0.625rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
/* Recent strip on the empty state — replaces the old persistent
|
|
|
|
|
|
conversation sidebar. Visible only while messages.length === 0. */
|
|
|
|
|
|
.chat-recent-strip {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 720px;
|
|
|
|
|
|
margin-top: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-recent-strip-label {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-recent-strip-kbd {
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
font-size: 0.65rem;
|
|
|
|
|
|
padding: 1px 5px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
text-transform: none;
|
|
|
|
|
|
letter-spacing: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-recent-strip-list {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-recent-strip-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
background: var(--color-surface-raised);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: border-color var(--duration-fast), background var(--duration-fast), transform var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-recent-strip-item:hover {
|
|
|
|
|
|
border-color: var(--color-primary-border);
|
|
|
|
|
|
background: var(--color-surface-raised);
|
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-recent-strip-item-name {
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-recent-strip-item-preview {
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-recent-strip-item-time {
|
|
|
|
|
|
font-size: 0.6875rem;
|
|
|
|
|
|
color: var(--color-text-tertiary);
|
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ChatsMenu — popover replacing the old persistent history sidebar. */
|
|
|
|
|
|
.chats-menu {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-trigger {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-trigger.active {
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
border-color: var(--color-primary-border);
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-trigger-label {
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-trigger-kbd {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
font-size: 0.65rem;
|
|
|
|
|
|
padding: 1px 5px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (min-width: 640px) {
|
|
|
|
|
|
.chats-menu-trigger-kbd { display: inline-block; }
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-popover {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: calc(100% + 4px);
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
z-index: 100;
|
|
|
|
|
|
width: 320px;
|
|
|
|
|
|
max-width: calc(100vw - var(--spacing-md) * 2);
|
|
|
|
|
|
background: var(--color-bg-primary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
box-shadow: var(--shadow-lg);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
max-height: min(70vh, 520px);
|
|
|
|
|
|
animation: dropdownIn 120ms ease-out;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-search {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-divider);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-search-icon {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 16px;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-search-input {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: 6px 26px 6px 28px;
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
transition: border-color var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-search-input:focus {
|
|
|
|
|
|
border-color: var(--color-primary-border);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-search-clear {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 14px;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
padding: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-list {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
padding: var(--spacing-xs);
|
|
|
|
|
|
scrollbar-width: thin;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-empty {
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
border-left: 2px solid transparent;
|
|
|
|
|
|
transition: background var(--duration-fast), color var(--duration-fast), border-color var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-item.highlighted {
|
|
|
|
|
|
background: var(--color-bg-hover);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-item.active {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
border-left-color: var(--color-primary);
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-item-icon {
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
color: var(--color-text-tertiary);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-item-info {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-item-top {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-item-name {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-item-spin {
|
|
|
|
|
|
margin-right: 6px;
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-item-time {
|
|
|
|
|
|
font-size: 0.65rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-item-preview {
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-item-rename {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
padding: 2px 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-item-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transition: opacity var(--duration-fast);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-item:hover .chats-menu-item-actions,
|
|
|
|
|
|
.chats-menu-item.highlighted .chats-menu-item-actions {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-item-actions button {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 2px 4px;
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-item-actions button:hover {
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-item-actions .chats-menu-item-delete:hover {
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-footer {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
padding: var(--spacing-xs);
|
|
|
|
|
|
border-top: 1px solid var(--color-border-divider);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chats-menu-new {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Mobile: popover becomes a full-width drawer anchored under the header. */
|
|
|
|
|
|
@media (max-width: 639px) {
|
|
|
|
|
|
.chats-menu-popover {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
top: 56px;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
border-radius: 0;
|
|
|
|
|
|
border-left: none;
|
|
|
|
|
|
border-right: none;
|
|
|
|
|
|
max-height: calc(100dvh - 56px);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Settings drawer — Manage mode toggle row at the top */
|
|
|
|
|
|
.chat-settings-toggle-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
padding: var(--spacing-sm) 0;
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-divider);
|
|
|
|
|
|
margin-bottom: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-settings-toggle-text {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-settings-toggle-title {
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-settings-toggle-title i {
|
|
|
|
|
|
color: var(--color-accent);
|
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-settings-toggle-desc {
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Settings drawer — destructive action area at the bottom */
|
|
|
|
|
|
.chat-settings-danger-zone {
|
|
|
|
|
|
margin-top: var(--spacing-md);
|
|
|
|
|
|
padding-top: var(--spacing-md);
|
|
|
|
|
|
border-top: 1px solid var(--color-border-divider);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-settings-danger-btn {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
padding: 8px var(--spacing-md);
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background var(--duration-fast), border-color var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-settings-danger-btn:hover {
|
|
|
|
|
|
background: rgba(220, 38, 38, 0.08);
|
|
|
|
|
|
border-color: var(--color-error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 22:42:47 +00:00
|
|
|
|
/* Activity group (thinking + tools collapsed into one line) */
|
|
|
|
|
|
@keyframes shimmer {
|
|
|
|
|
|
0% { background-position: -200% 0; }
|
|
|
|
|
|
100% { background-position: 200% 0; }
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-03-09 22:42:47 +00:00
|
|
|
|
|
|
|
|
|
|
.chat-activity-group {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
border-left: 2px solid var(--color-border-subtle);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-03-09 22:42:47 +00:00
|
|
|
|
.chat-activity-streaming {
|
|
|
|
|
|
border-left-color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-activity-toggle {
|
2026-03-05 20:47:12 +00:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
2026-03-09 22:42:47 +00:00
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: 6px 12px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-family: inherit;
|
2026-03-09 22:42:47 +00:00
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
transition: color 150ms;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
text-align: left;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-03-09 22:42:47 +00:00
|
|
|
|
.chat-activity-toggle:hover {
|
|
|
|
|
|
color: var(--color-text-secondary);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-03-09 22:42:47 +00:00
|
|
|
|
.chat-activity-toggle i {
|
|
|
|
|
|
font-size: 0.5rem;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
opacity: 0.4;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-activity-summary {
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
letter-spacing: 0.01em;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-activity-count {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
margin-left: 6px;
|
|
|
|
|
|
padding: 0 5px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
font-size: 0.6rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-activity-shimmer {
|
|
|
|
|
|
background: linear-gradient(
|
|
|
|
|
|
90deg,
|
|
|
|
|
|
var(--color-text-muted) 0%,
|
|
|
|
|
|
var(--color-text-muted) 40%,
|
|
|
|
|
|
var(--color-primary) 50%,
|
|
|
|
|
|
var(--color-text-muted) 60%,
|
|
|
|
|
|
var(--color-text-muted) 100%
|
|
|
|
|
|
);
|
|
|
|
|
|
background-size: 200% 100%;
|
|
|
|
|
|
-webkit-background-clip: text;
|
|
|
|
|
|
background-clip: text;
|
|
|
|
|
|
-webkit-text-fill-color: transparent;
|
|
|
|
|
|
animation: shimmer 3s ease-in-out infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-activity-details {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
padding: 2px 0 6px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-activity-item {
|
|
|
|
|
|
padding: 3px 12px;
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 1px;
|
|
|
|
|
|
border-left: 2px solid transparent;
|
|
|
|
|
|
margin-left: -2px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-activity-item-label {
|
|
|
|
|
|
font-size: 0.575rem;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
font-weight: 600;
|
2026-03-09 22:42:47 +00:00
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.04em;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-03-09 22:42:47 +00:00
|
|
|
|
.chat-activity-item-text {
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-activity-item-content {
|
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
max-height: 200px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
overflow-x: hidden;
|
2026-03-09 22:42:47 +00:00
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
overflow-wrap: anywhere;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-activity-item-content.chat-activity-live {
|
|
|
|
|
|
max-height: 300px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-03-09 22:42:47 +00:00
|
|
|
|
.chat-activity-item-content p { margin: 0 0 4px; }
|
|
|
|
|
|
.chat-activity-item-content p:last-child { margin-bottom: 0; }
|
|
|
|
|
|
.chat-activity-item-content pre {
|
2026-03-05 20:47:12 +00:00
|
|
|
|
background: var(--color-bg-tertiary);
|
2026-03-09 22:42:47 +00:00
|
|
|
|
padding: var(--spacing-xs);
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
overflow-x: auto;
|
2026-03-09 22:42:47 +00:00
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
word-break: break-word;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-03-09 22:42:47 +00:00
|
|
|
|
.chat-activity-item-content code {
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
overflow-wrap: anywhere;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-03-09 22:42:47 +00:00
|
|
|
|
.chat-activity-item-code {
|
|
|
|
|
|
margin: 2px 0 0;
|
|
|
|
|
|
font-size: 0.65rem;
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
max-height: 120px;
|
|
|
|
|
|
overflow-y: auto;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-03-09 22:42:47 +00:00
|
|
|
|
.chat-activity-item-code code {
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
font-family: var(--font-mono);
|
2026-03-09 22:42:47 +00:00
|
|
|
|
font-size: 0.65rem;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-03-09 22:42:47 +00:00
|
|
|
|
.chat-activity-params {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 3px;
|
|
|
|
|
|
margin-top: 2px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-03-09 22:42:47 +00:00
|
|
|
|
.chat-activity-param {
|
2026-03-05 20:47:12 +00:00
|
|
|
|
display: flex;
|
2026-03-09 22:42:47 +00:00
|
|
|
|
gap: 6px;
|
|
|
|
|
|
font-size: 0.675rem;
|
|
|
|
|
|
line-height: 1.4;
|
|
|
|
|
|
word-break: break-word;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-03-09 22:42:47 +00:00
|
|
|
|
.chat-activity-param-key {
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
opacity: 0.7;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-03-09 22:42:47 +00:00
|
|
|
|
.chat-activity-param-val {
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
white-space: pre-wrap;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
min-width: 0;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-03-09 22:42:47 +00:00
|
|
|
|
.chat-activity-param-val-long {
|
|
|
|
|
|
max-height: 80px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
2026-03-09 22:42:47 +00:00
|
|
|
|
.chat-activity-thinking {
|
2026-04-18 23:01:01 +00:00
|
|
|
|
border-left-color: var(--color-info-border);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-03-09 22:42:47 +00:00
|
|
|
|
.chat-activity-tool-call {
|
2026-04-18 23:01:01 +00:00
|
|
|
|
border-left-color: var(--color-warning-border);
|
2026-03-09 22:42:47 +00:00
|
|
|
|
}
|
|
|
|
|
|
.chat-activity-tool-result {
|
2026-04-18 23:01:01 +00:00
|
|
|
|
border-left-color: var(--color-success-border);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Context window progress bar */
|
|
|
|
|
|
.chat-context-bar {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
height: 18px;
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-context-progress {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
transition: width 300ms ease;
|
|
|
|
|
|
opacity: 0.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-context-label {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
z-index: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
font-size: 0.625rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Chat header */
|
|
|
|
|
|
.chat-header {
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
padding: var(--spacing-xs) var(--spacing-md);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border-bottom: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
gap: var(--spacing-xs);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
flex-shrink: 0;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
min-height: 40px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
.chat-header-title {
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
font-size: 0.8125rem;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
flex: 0 1 auto;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-header-shield {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 22px;
|
|
|
|
|
|
height: 22px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--color-accent);
|
|
|
|
|
|
background: var(--color-accent-light);
|
|
|
|
|
|
flex-shrink: 0;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
.chat-header-actions {
|
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
flex-shrink: 0;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
.chat-header-actions .btn-secondary.active {
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
border-color: var(--color-primary-border);
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
}
|
feat: react chat redesign (#9616)
* feat(react-ui): redesign chat — popover history, focus on send, density pass
Replace the persistent 260px conversation sidebar with a Cmd/Ctrl+K
popover (ChatsMenu) so the conversation owns the page. Once a chat has
at least one message we auto-collapse the global app rail and fade
non-essential header chrome; Esc gives the user back the full chrome
for the rest of the session.
Move Canvas mode and the MCP dropdown into the input wrapper as mode
chips — they describe what's armed for the next message and now live
where the user composes. The chat header drops to Chats · title ·
ModelSelector · overflow · settings, and an overflow menu carries
admin-only Manage mode along with Info / Edit / Export / Clear.
Density pass: tighter header (40px), smaller avatars with the assistant
left-border accent doing the work, 88% bubble width, modern
field-sizing on the textarea, 32px send/stop buttons.
Empty state now surfaces a Recent strip (top 4 non-empty chats) and a
Cmd+K hint, replacing the discoverability the persistent sidebar used
to provide.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* feat(react-ui): chat input chips, slimmer menu, focus mode polish
Move Canvas mode and the MCP dropdown into the input wrapper as compact
mode chips — they describe what's armed for the next message and now
sit where the user composes. The MCP popover flips upward when anchored
to the input row so it stays on-screen.
Eliminate the chat header overflow ("…") menu entirely; relocate each
item to its semantic home so users don't have to remember a
miscellany drawer:
- Manage mode toggle → top of the Settings drawer, alongside the
other sticky chat knobs. The shield next to the title still
signals state at a glance.
- Model info / Edit config → small admin-only "ⓘ" button next to the
ModelSelector; the existing model-info panel now hosts the Edit
config link.
- Export as Markdown → per-row hover action in ChatsMenu, so it works
for any chat (not just the active one).
- Clear chat history → destructive button at the bottom of the
Settings drawer.
Make the Sidebar listen to its own `sidebar-collapse` event so the
chat's focus mode actually shrinks the rail (it previously only
flipped the layout class, leaving the sidebar element at full width
and overlapping the chat). Drop the focus-mode toast — the visual
shift is enough; the toast was noise.
Define `--color-text-tertiary` in both themes; without it metadata
text (recent strip timestamps and a few other sites) was inheriting
the platform default, which read as black on the dark surface.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* fix(model/log-store): close merged channel exactly once; clean up Remove
Two latent races in BackendLogStore.Subscribe could panic under load
(distributed e2e test triggered "send on closed channel" at
backend_log_store.go:288):
1. The aggregated path closed the merged channel `ch` from two
places — the fan-in waiter goroutine (after all source channels
drained) and unsubscribe(). When unsubscribe ran while a fan-in
goroutine was mid-flight on `ch <- line`, the close beat the send
and the runtime panicked. Now `ch` is closed by exactly one
goroutine: the waiter that observes all fan-in goroutines finish.
unsubscribe() only closes the per-buffer source channels — the
for-range in each fan-in goroutine then exits naturally and the
waiter takes care of the merged close.
2. Remove() closed every subscriber channel but didn't delete the
entries from the subscribers map, so a concurrent unsubscribe()
would call close() again on the already-closed channel
("close of closed channel"). Clear the map entry while closing.
Add a regression test that hammers AppendLine concurrently with
Subscribe + unsubscribe + Remove; the race detector catches both
classes of regression.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
* test(model/log-store): port backend log store tests to ginkgo
Bring backend_log_store_test.go in line with the rest of pkg/model
(loader_test, watchdog_test, store_test): same external test package
(`model_test`), same ginkgo + gomega imports, same Describe/It
nesting around the public API. Behaviour is unchanged — the four
existing scenarios plus the unsubscribe race regression all run as
specs under the existing `TestModel` suite.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-29 20:33:26 +00:00
|
|
|
|
|
|
|
|
|
|
/* Focus mode: once a conversation is underway, fade non-essential
|
|
|
|
|
|
header chrome and pull the messages padding tighter. Hover or focus
|
|
|
|
|
|
on the header brings everything back; Esc removes focus mode for the
|
|
|
|
|
|
rest of the session. */
|
|
|
|
|
|
.chat--focus .chat-header-title,
|
|
|
|
|
|
.chat--focus .chat-header-shield {
|
|
|
|
|
|
opacity: 0.45;
|
|
|
|
|
|
transition: opacity var(--duration-normal) var(--ease-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat--focus .chat-header:hover .chat-header-title,
|
|
|
|
|
|
.chat--focus .chat-header:hover .chat-header-shield,
|
|
|
|
|
|
.chat--focus .chat-header:focus-within .chat-header-title,
|
|
|
|
|
|
.chat--focus .chat-header:focus-within .chat-header-shield {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat--focus .chat-messages {
|
|
|
|
|
|
padding-top: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
|
.chat--focus .chat-header-title,
|
|
|
|
|
|
.chat--focus .chat-header-shield,
|
|
|
|
|
|
.chat-messages { transition: none; }
|
|
|
|
|
|
}
|
2026-03-11 06:30:49 +00:00
|
|
|
|
/* Chat MCP dropdown */
|
|
|
|
|
|
.chat-mcp-dropdown {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mcp-dropdown .btn {
|
2026-03-05 20:47:12 +00:00
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-03-11 06:30:49 +00:00
|
|
|
|
gap: 5px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-03-11 06:30:49 +00:00
|
|
|
|
.chat-mcp-badge {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
min-width: 18px;
|
|
|
|
|
|
height: 18px;
|
|
|
|
|
|
padding: 0 5px;
|
|
|
|
|
|
border-radius: 9px;
|
|
|
|
|
|
background: rgba(255,255,255,0.25);
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
line-height: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mcp-dropdown-menu {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: calc(100% + 4px);
|
2026-03-18 20:14:41 +00:00
|
|
|
|
left: 0;
|
2026-03-11 06:30:49 +00:00
|
|
|
|
z-index: 100;
|
|
|
|
|
|
min-width: 240px;
|
|
|
|
|
|
max-height: 320px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
background: var(--color-bg-primary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
box-shadow: var(--shadow-lg);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
animation: dropdownIn 120ms ease-out;
|
2026-03-11 06:30:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
.chat-mcp-dropdown-loading,
|
|
|
|
|
|
.chat-mcp-dropdown-empty {
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mcp-dropdown-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-md);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-divider);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
font-size: 0.75rem;
|
2026-03-11 06:30:49 +00:00
|
|
|
|
font-weight: 600;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
color: var(--color-text-secondary);
|
2026-03-11 06:30:49 +00:00
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.03em;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mcp-select-all {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--color-accent);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
text-transform: none;
|
|
|
|
|
|
letter-spacing: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mcp-select-all:hover {
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mcp-server-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-md);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: background 120ms;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mcp-server-item:hover {
|
|
|
|
|
|
background: var(--color-bg-hover);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mcp-server-item input[type="checkbox"] {
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mcp-server-info {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 1px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mcp-server-name {
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-mcp-server-tools {
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
color: var(--color-text-tertiary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Client MCP status indicators */
|
|
|
|
|
|
.chat-client-mcp-status {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
width: 8px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
background: var(--color-text-tertiary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-client-mcp-status-connected {
|
2026-03-19 20:40:51 +00:00
|
|
|
|
background: var(--color-success);
|
2026-03-11 06:30:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
.chat-client-mcp-status-connecting {
|
2026-03-19 20:40:51 +00:00
|
|
|
|
background: var(--color-warning);
|
2026-03-11 06:30:49 +00:00
|
|
|
|
animation: pulse 1s infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-client-mcp-status-error {
|
2026-03-19 20:40:51 +00:00
|
|
|
|
background: var(--color-error);
|
2026-03-11 06:30:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
.chat-client-mcp-status-disconnected {
|
|
|
|
|
|
background: var(--color-text-tertiary);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Chat model info panel */
|
|
|
|
|
|
.chat-model-info-panel {
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
animation: fadeIn 150ms ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-model-info-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-md);
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-divider);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-model-info-body {
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
max-height: 200px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-model-info-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
padding: 2px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-model-info-row > span:first-child {
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-model-info-row > span:last-child {
|
|
|
|
|
|
color: var(--color-text-primary);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
font-family: var(--font-mono);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
max-width: 60%;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Settings drawer */
|
|
|
|
|
|
.chat-settings-overlay {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.3);
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
transition: opacity 200ms;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-settings-overlay.open {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
pointer-events: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-settings-drawer {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
right: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
width: 320px;
|
|
|
|
|
|
max-width: 90%;
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
border-left: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
z-index: 11;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
transform: translateX(100%);
|
|
|
|
|
|
transition: transform 250ms var(--ease-default);
|
|
|
|
|
|
box-shadow: var(--shadow-lg);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
will-change: transform;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
.chat-settings-drawer.open {
|
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-settings-drawer-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-settings-drawer-body {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Chat search */
|
|
|
|
|
|
.chat-search-wrapper {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
margin-bottom: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-search-icon {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 8px;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-search-input {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
padding: 5px 24px 5px 26px;
|
|
|
|
|
|
background: var(--color-bg-primary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
transition: border-color 150ms;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-search-input:focus {
|
|
|
|
|
|
border-color: var(--color-primary-border);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-search-input::placeholder {
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-search-clear {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 6px;
|
|
|
|
|
|
top: 50%;
|
|
|
|
|
|
transform: translateY(-50%);
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
padding: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Chat list item actions */
|
|
|
|
|
|
.chat-list-item-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transition: opacity 150ms;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-list-item:hover .chat-list-item-actions {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-list-item-actions button {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 2px;
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-list-item-actions button:hover {
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-list-item-actions .chat-list-item-delete:hover {
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Max tokens/sec badge */
|
|
|
|
|
|
.chat-max-tps-badge {
|
2026-03-19 20:40:51 +00:00
|
|
|
|
background: rgba(59, 130, 246, 0.15);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
padding: 1px 6px;
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Slider styles */
|
|
|
|
|
|
.chat-slider {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 4px;
|
|
|
|
|
|
appearance: none;
|
|
|
|
|
|
-webkit-appearance: none;
|
2026-03-18 07:31:26 +00:00
|
|
|
|
background: var(--color-border-default);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-slider::-webkit-slider-thumb {
|
|
|
|
|
|
-webkit-appearance: none;
|
|
|
|
|
|
width: 14px;
|
|
|
|
|
|
height: 14px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: var(--color-primary);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-slider::-moz-range-thumb {
|
|
|
|
|
|
width: 14px;
|
|
|
|
|
|
height: 14px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: var(--color-primary);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-slider-labels {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
font-size: 0.625rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Message inline files */
|
|
|
|
|
|
.chat-message-files {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
margin-top: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-file-inline {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
padding: 2px 6px;
|
2026-04-18 23:01:01 +00:00
|
|
|
|
background: color-mix(in oklab, currentColor 12%, transparent);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.chat-inline-image {
|
|
|
|
|
|
max-width: 200px;
|
|
|
|
|
|
max-height: 200px;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
margin-top: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-files {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-lg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-file-badge {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-file-badge button {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
font-size: 0.625rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-21 01:08:02 +00:00
|
|
|
|
/* Studio tabs */
|
|
|
|
|
|
.studio-tabs {
|
|
|
|
|
|
display: flex;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
gap: var(--spacing-xs);
|
2026-03-21 01:08:02 +00:00
|
|
|
|
border-bottom: 1px solid var(--color-border-subtle);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
padding: var(--spacing-sm) var(--spacing-xl) 0;
|
2026-03-21 01:08:02 +00:00
|
|
|
|
background: var(--color-bg-primary);
|
|
|
|
|
|
position: sticky;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
z-index: 10;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.studio-tab {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
gap: var(--spacing-xs);
|
2026-03-21 01:08:02 +00:00
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
padding: 10px var(--spacing-md);
|
|
|
|
|
|
font-size: var(--text-sm);
|
2026-03-21 01:08:02 +00:00
|
|
|
|
font-family: inherit;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-weight: var(--font-weight-medium);
|
2026-03-21 01:08:02 +00:00
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
border-bottom: 2px solid transparent;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
margin-bottom: -1px;
|
2026-03-21 01:08:02 +00:00
|
|
|
|
transition: color var(--duration-fast), border-color var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.studio-tab:hover {
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.studio-tab-active {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
border-bottom-color: var(--color-primary);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-weight: var(--font-weight-semibold);
|
2026-03-21 01:08:02 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
/* Two-column layout for media generation pages */
|
|
|
|
|
|
.media-layout {
|
|
|
|
|
|
display: grid;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
grid-template-columns: minmax(320px, 420px) 1fr;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
gap: var(--spacing-lg);
|
|
|
|
|
|
padding: var(--spacing-xl);
|
2026-04-27 11:51:29 +00:00
|
|
|
|
max-width: var(--page-max-default);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
align-items: start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-14 21:53:03 +00:00
|
|
|
|
@media (max-width: 900px) {
|
|
|
|
|
|
.media-layout {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.media-controls {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
background: var(--color-surface-raised);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
padding: var(--spacing-lg);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
box-shadow: var(--shadow-subtle);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
position: sticky;
|
|
|
|
|
|
top: var(--spacing-lg);
|
|
|
|
|
|
}
|
2026-04-14 21:53:03 +00:00
|
|
|
|
.media-controls .form-group {
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.media-controls .form-grid-2col,
|
|
|
|
|
|
.media-controls .form-grid-3col {
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.media-controls .form-grid-2col .form-group,
|
|
|
|
|
|
.media-controls .form-grid-3col .form-group {
|
|
|
|
|
|
margin-bottom: 0;
|
|
|
|
|
|
}
|
2026-03-05 20:47:12 +00:00
|
|
|
|
|
|
|
|
|
|
.media-controls .page-header {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
margin-bottom: var(--spacing-lg);
|
|
|
|
|
|
padding-bottom: var(--spacing-md);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-divider);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-controls .page-title {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-lg);
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-controls .page-title i {
|
|
|
|
|
|
color: var(--color-accent);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-preview {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-result {
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
padding: var(--spacing-lg);
|
|
|
|
|
|
min-height: 320px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-result img,
|
|
|
|
|
|
.media-result video {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-result-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-29 22:49:55 +00:00
|
|
|
|
/* Media generation history */
|
|
|
|
|
|
.media-history {
|
|
|
|
|
|
margin-top: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-history-clear-btn {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
transition: color var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-history-clear-btn:hover {
|
|
|
|
|
|
color: var(--color-danger);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-history-list {
|
|
|
|
|
|
max-height: 400px;
|
|
|
|
|
|
overflow-y: auto;
|
|
|
|
|
|
padding: var(--spacing-xs) 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-history-empty {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-history-item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
padding: var(--spacing-sm);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
transition: background var(--duration-fast), transform var(--duration-fast);
|
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-history-item:hover {
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
transform: translateX(2px);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-history-item.active {
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-history-item-thumb {
|
|
|
|
|
|
width: 32px;
|
|
|
|
|
|
height: 32px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-history-item-thumb img {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-history-item-info {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-history-item-top {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-history-item-prompt {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-history-item-time {
|
|
|
|
|
|
font-size: 0.625rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-history-item-model {
|
|
|
|
|
|
font-size: 0.6875rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
line-height: 1.3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-history-item-delete {
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 2px;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
transition: opacity var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-history-item:hover .media-history-item-delete {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.media-history-item-delete:hover {
|
|
|
|
|
|
color: var(--color-danger);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 11:51:29 +00:00
|
|
|
|
/* ============================================================
|
|
|
|
|
|
Responsive
|
|
|
|
|
|
------------------------------------------------------------
|
|
|
|
|
|
Three viewport tiers, expressed as cascading media queries:
|
|
|
|
|
|
|
|
|
|
|
|
* desktop (≥1024px) — full sidebar, content margin-left = sidebar-width
|
|
|
|
|
|
* tablet (640–1023) — 52px icon rail; tap hamburger to overlay-expand
|
|
|
|
|
|
* mobile (<640px) — sidebar slides off-screen behind a top-bar drawer
|
|
|
|
|
|
|
|
|
|
|
|
Touch-target minimums apply across both tablet and mobile via the
|
|
|
|
|
|
first (max-width: 1023px) block.
|
|
|
|
|
|
============================================================ */
|
|
|
|
|
|
|
|
|
|
|
|
/* Touch-friendly sizing + shared layout simplifications (tablet + mobile) */
|
2026-03-05 20:47:12 +00:00
|
|
|
|
@media (max-width: 1023px) {
|
2026-04-27 11:51:29 +00:00
|
|
|
|
.hamburger-btn {
|
|
|
|
|
|
min-width: 44px;
|
|
|
|
|
|
min-height: 44px;
|
|
|
|
|
|
padding: 10px 12px;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 11:51:29 +00:00
|
|
|
|
.nav-item {
|
|
|
|
|
|
min-height: 44px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 11:51:29 +00:00
|
|
|
|
.sidebar-close-btn {
|
|
|
|
|
|
min-width: 44px;
|
|
|
|
|
|
min-height: 44px;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
padding: 10px;
|
2026-03-11 06:30:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 11:51:29 +00:00
|
|
|
|
.mobile-header {
|
|
|
|
|
|
min-height: 56px;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 11:51:29 +00:00
|
|
|
|
/* Layouts that need to collapse to single-column on any narrow viewport */
|
|
|
|
|
|
.chat-sidebar { display: none; }
|
|
|
|
|
|
.chat-settings-drawer {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 100%;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-04-27 11:51:29 +00:00
|
|
|
|
.media-layout { grid-template-columns: 1fr; }
|
|
|
|
|
|
.media-controls { position: static; }
|
|
|
|
|
|
.page { padding: var(--spacing-md); }
|
2026-03-05 20:47:12 +00:00
|
|
|
|
|
2026-04-27 11:51:29 +00:00
|
|
|
|
/* The desktop collapse chevron is desktop-only — tablets auto-rail,
|
|
|
|
|
|
mobile uses the hamburger. */
|
|
|
|
|
|
.sidebar-collapse-btn { display: none; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Tablet (640–1023): persistent icon rail; tap hamburger to overlay-expand */
|
|
|
|
|
|
@media (max-width: 1023px) and (min-width: 640px) {
|
|
|
|
|
|
.main-content,
|
|
|
|
|
|
.sidebar-is-collapsed .main-content {
|
|
|
|
|
|
margin-left: var(--sidebar-width-collapsed);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 11:51:29 +00:00
|
|
|
|
.mobile-header { display: none; }
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar {
|
|
|
|
|
|
width: var(--sidebar-width-collapsed);
|
|
|
|
|
|
transform: translateX(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
.sidebar.collapsed { width: var(--sidebar-width-collapsed); }
|
|
|
|
|
|
|
|
|
|
|
|
/* Apply collapsed visuals while not pinned-open. These mirror the
|
|
|
|
|
|
existing .sidebar.collapsed desktop rules so we re-use one look. */
|
|
|
|
|
|
.sidebar:not(.open) .nav-label,
|
|
|
|
|
|
.sidebar:not(.open) .nav-external,
|
|
|
|
|
|
.sidebar:not(.open) .sidebar-section-title,
|
|
|
|
|
|
.sidebar:not(.open) .sidebar-section-chevron { display: none; }
|
|
|
|
|
|
.sidebar:not(.open) .sidebar-logo-link { display: none; }
|
|
|
|
|
|
.sidebar:not(.open) .sidebar-logo-icon {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
.sidebar:not(.open) .sidebar-header { justify-content: center; }
|
|
|
|
|
|
.sidebar:not(.open) .nav-item {
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
|
border-left-width: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.sidebar:not(.open) .nav-icon { width: auto; font-size: 1rem; }
|
|
|
|
|
|
.sidebar:not(.open) .sidebar-footer {
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
2026-03-11 06:30:49 +00:00
|
|
|
|
}
|
2026-04-27 11:51:29 +00:00
|
|
|
|
.sidebar:not(.open) .sidebar-user-name,
|
|
|
|
|
|
.sidebar:not(.open) .sidebar-logout-btn { display: none; }
|
2026-03-11 06:30:49 +00:00
|
|
|
|
|
2026-04-27 11:51:29 +00:00
|
|
|
|
/* Pinned open: overlay the full sidebar on top of content */
|
|
|
|
|
|
.sidebar.open {
|
|
|
|
|
|
width: var(--sidebar-width);
|
|
|
|
|
|
box-shadow: var(--shadow-md);
|
2026-03-11 06:30:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 11:51:29 +00:00
|
|
|
|
.sidebar-close-btn { display: none; }
|
|
|
|
|
|
.sidebar.open .sidebar-close-btn { display: flex; }
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-overlay {
|
2026-03-11 06:30:49 +00:00
|
|
|
|
display: block;
|
2026-04-27 11:51:29 +00:00
|
|
|
|
position: fixed;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.5);
|
|
|
|
|
|
z-index: 40;
|
2026-03-11 06:30:49 +00:00
|
|
|
|
}
|
2026-04-27 11:51:29 +00:00
|
|
|
|
}
|
2026-03-11 06:30:49 +00:00
|
|
|
|
|
2026-04-27 11:51:29 +00:00
|
|
|
|
/* Mobile (<640): sidebar slides off-screen as a drawer */
|
|
|
|
|
|
@media (max-width: 639px) {
|
|
|
|
|
|
.main-content,
|
|
|
|
|
|
.sidebar-is-collapsed .main-content {
|
|
|
|
|
|
margin-left: 0;
|
2026-03-11 06:30:49 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 11:51:29 +00:00
|
|
|
|
.mobile-header { display: flex; }
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar {
|
|
|
|
|
|
transform: translateX(-100%);
|
|
|
|
|
|
width: var(--sidebar-width);
|
|
|
|
|
|
}
|
|
|
|
|
|
.sidebar.collapsed { width: var(--sidebar-width); }
|
|
|
|
|
|
.sidebar.open { transform: translateX(0); }
|
|
|
|
|
|
|
|
|
|
|
|
.sidebar-close-btn { display: flex; }
|
|
|
|
|
|
|
|
|
|
|
|
/* When opened on mobile, even if the .collapsed class is present
|
|
|
|
|
|
from desktop preference, force the expanded look — drawer always
|
|
|
|
|
|
shows full labels. */
|
|
|
|
|
|
.sidebar.collapsed .nav-label,
|
|
|
|
|
|
.sidebar.collapsed .nav-external,
|
|
|
|
|
|
.sidebar.collapsed .sidebar-section-title { display: unset; }
|
|
|
|
|
|
.sidebar.collapsed .sidebar-logo-link { display: block; }
|
|
|
|
|
|
.sidebar.collapsed .sidebar-logo-icon { display: none; }
|
2026-03-11 06:30:49 +00:00
|
|
|
|
.sidebar.collapsed .nav-item {
|
|
|
|
|
|
justify-content: flex-start;
|
2026-04-27 11:51:29 +00:00
|
|
|
|
padding: 10px var(--spacing-md);
|
2026-03-11 06:30:49 +00:00
|
|
|
|
border-left-width: 3px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.sidebar.collapsed .nav-icon {
|
|
|
|
|
|
width: 18px;
|
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
}
|
2026-04-27 11:51:29 +00:00
|
|
|
|
.sidebar.collapsed .sidebar-header { justify-content: space-between; }
|
2026-03-11 06:30:49 +00:00
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
.sidebar-overlay {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
background: rgba(0, 0, 0, 0.5);
|
|
|
|
|
|
z-index: 40;
|
|
|
|
|
|
}
|
2026-04-27 11:51:29 +00:00
|
|
|
|
}
|
2026-03-05 20:47:12 +00:00
|
|
|
|
|
2026-04-27 11:51:29 +00:00
|
|
|
|
/* Mobile reflow polish — phone-only (<640) layout adjustments for
|
|
|
|
|
|
page chrome that was designed flex-row on desktop. */
|
|
|
|
|
|
@media (max-width: 639px) {
|
|
|
|
|
|
/* Page header: stack title block + inline-action cluster vertically */
|
|
|
|
|
|
.page-header {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
|
gap: var(--spacing-md);
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 11:51:29 +00:00
|
|
|
|
/* Filter chip rows scroll horizontally instead of wrapping into walls */
|
|
|
|
|
|
.filter-bar {
|
|
|
|
|
|
flex-wrap: nowrap;
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
|
|
|
|
|
scrollbar-width: none;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-04-27 11:51:29 +00:00
|
|
|
|
.filter-bar::-webkit-scrollbar { display: none; }
|
|
|
|
|
|
.filter-btn { flex-shrink: 0; }
|
|
|
|
|
|
|
|
|
|
|
|
.search-bar { min-width: 0; }
|
|
|
|
|
|
|
|
|
|
|
|
/* Tables go edge-to-edge; offsetting against .page padding gives full
|
|
|
|
|
|
bleed without changing the table layout itself. */
|
|
|
|
|
|
.table-container {
|
|
|
|
|
|
border-radius: 0;
|
|
|
|
|
|
border-left: 0;
|
|
|
|
|
|
border-right: 0;
|
|
|
|
|
|
margin-inline: calc(-1 * var(--spacing-md));
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-27 11:51:29 +00:00
|
|
|
|
/* Operations toasts: scroll horizontally instead of wrapping */
|
|
|
|
|
|
.operations-bar {
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
flex-wrap: nowrap;
|
|
|
|
|
|
-webkit-overflow-scrolling: touch;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-04-27 11:51:29 +00:00
|
|
|
|
.operation-item { flex-shrink: 0; }
|
|
|
|
|
|
.operation-text {
|
|
|
|
|
|
max-width: 60vw;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-03-05 20:47:12 +00:00
|
|
|
|
|
2026-04-27 11:51:29 +00:00
|
|
|
|
/* Reduced motion — disable non-essential transitions for users who
|
|
|
|
|
|
request it. Keeps focus/state changes accessible without animation. */
|
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
|
.sidebar,
|
|
|
|
|
|
.page-transition,
|
|
|
|
|
|
.operations-bar,
|
|
|
|
|
|
.page,
|
|
|
|
|
|
.main-content {
|
|
|
|
|
|
transition: none !important;
|
|
|
|
|
|
animation: none !important;
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-09 22:42:47 +00:00
|
|
|
|
/* Canvas panel */
|
|
|
|
|
|
.canvas-panel {
|
|
|
|
|
|
width: 45%;
|
|
|
|
|
|
max-width: 720px;
|
|
|
|
|
|
flex-shrink: 1;
|
|
|
|
|
|
border-left: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
background: var(--color-bg-primary);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-panel-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-panel-title {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-panel-tabs {
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-md);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
scrollbar-width: thin;
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-panel-tab {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
|
border-radius: 999px;
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
transition: all 150ms;
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-panel-tab:hover { border-color: var(--color-border-default); }
|
|
|
|
|
|
.canvas-panel-tab.active {
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
border-color: var(--color-primary);
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-panel-tab span {
|
|
|
|
|
|
max-width: 100px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-panel-toolbar {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-md);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-toggle-group {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
border: 1px solid var(--color-border-default);
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-toggle-btn {
|
|
|
|
|
|
padding: 2px 10px;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 150ms;
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-toggle-btn.active {
|
|
|
|
|
|
background: var(--color-primary);
|
|
|
|
|
|
color: var(--color-primary-text);
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-panel-body {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
min-height: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-panel-body pre {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Artifact card (inline in messages) */
|
|
|
|
|
|
.artifact-card {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
border: 1px solid var(--color-border-default);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
margin: var(--spacing-sm) 0;
|
|
|
|
|
|
transition: border-color 150ms;
|
|
|
|
|
|
}
|
|
|
|
|
|
.artifact-card:hover {
|
|
|
|
|
|
border-color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.artifact-card-icon {
|
|
|
|
|
|
font-size: 1.1rem;
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.artifact-card-info {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.artifact-card-title {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.artifact-card-lang {
|
|
|
|
|
|
font-size: 0.7rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
.artifact-card-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.artifact-card-actions button {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 4px 6px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
transition: all 150ms;
|
|
|
|
|
|
}
|
|
|
|
|
|
.artifact-card-actions button:hover {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Resource cards (below agent messages) */
|
|
|
|
|
|
.resource-cards {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
margin-top: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-card {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
transition: border-color 150ms;
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-card:hover {
|
|
|
|
|
|
border-color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-card-thumb {
|
|
|
|
|
|
width: 40px;
|
|
|
|
|
|
height: 40px;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-card-label {
|
|
|
|
|
|
max-width: 120px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-cards-more {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: 1px dashed var(--color-border-default);
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-cards-more:hover {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
border-color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Canvas preview types */
|
|
|
|
|
|
.canvas-preview-iframe {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
min-height: 600px;
|
|
|
|
|
|
height: calc(100vh - 200px);
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-preview-image {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-preview-svg {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-preview-svg svg {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-preview-markdown {
|
|
|
|
|
|
padding: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-audio-wrapper {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
padding: var(--spacing-lg);
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-audio-icon {
|
|
|
|
|
|
font-size: 2rem;
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-url-card {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-url-card a {
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Canvas mode toggle */
|
|
|
|
|
|
.canvas-mode-toggle {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-mode-toggle .canvas-mode-label {
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
.canvas-mode-toggle .toggle {
|
|
|
|
|
|
transform: scale(0.8);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
|
|
|
|
.canvas-panel {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
z-index: 50;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-05 20:47:12 +00:00
|
|
|
|
@media (max-width: 640px) {
|
|
|
|
|
|
.card-grid {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.filter-bar {
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
flex-wrap: nowrap;
|
|
|
|
|
|
padding-bottom: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-header {
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-header-title {
|
|
|
|
|
|
max-width: 120px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.chat-empty-hints {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
2026-03-19 20:40:51 +00:00
|
|
|
|
|
|
|
|
|
|
.chat-empty-suggestions {
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
|
}
|
2026-03-05 20:47:12 +00:00
|
|
|
|
}
|
2026-03-11 06:30:49 +00:00
|
|
|
|
|
|
|
|
|
|
/* MCP App Frame */
|
|
|
|
|
|
.mcp-app-frame-container {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
margin: var(--spacing-sm) 0;
|
|
|
|
|
|
border-radius: var(--border-radius-md);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mcp-app-iframe {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
min-height: 100px;
|
|
|
|
|
|
max-height: 600px;
|
|
|
|
|
|
transition: height 0.2s ease;
|
|
|
|
|
|
background: var(--color-bg-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mcp-app-error {
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
color: var(--color-text-danger, #e53e3e);
|
|
|
|
|
|
font-size: 0.85rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.mcp-app-reconnect-overlay {
|
|
|
|
|
|
padding: var(--spacing-sm);
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-size: 0.8rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
border-top: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
}
|
2026-03-19 20:40:51 +00:00
|
|
|
|
|
|
|
|
|
|
/* Confirm Dialog */
|
|
|
|
|
|
.confirm-dialog-backdrop {
|
|
|
|
|
|
position: fixed;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
z-index: 1050;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
background: var(--color-modal-backdrop);
|
|
|
|
|
|
backdrop-filter: blur(4px);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
animation: fadeIn var(--duration-normal) var(--ease-spring);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.confirm-dialog {
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
border: 1px solid var(--color-border-strong);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
max-width: 420px;
|
|
|
|
|
|
width: 90%;
|
|
|
|
|
|
padding: var(--spacing-lg);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
box-shadow: var(--shadow-md);
|
|
|
|
|
|
animation: popIn var(--duration-slow) var(--ease-spring);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
will-change: transform, opacity;
|
|
|
|
|
|
}
|
|
|
|
|
|
@keyframes slideUp {
|
|
|
|
|
|
from { opacity: 0; transform: translateY(8px); }
|
|
|
|
|
|
to { opacity: 1; transform: translateY(0); }
|
|
|
|
|
|
}
|
|
|
|
|
|
.confirm-dialog-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.confirm-dialog-danger-icon {
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
font-size: 1.125rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.confirm-dialog-title {
|
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.confirm-dialog-body {
|
|
|
|
|
|
font-size: 0.875rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
margin-bottom: var(--spacing-lg);
|
|
|
|
|
|
line-height: 1.5;
|
|
|
|
|
|
}
|
|
|
|
|
|
.confirm-dialog-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
.confirm-dialog-actions .btn-danger {
|
2026-03-19 20:40:51 +00:00
|
|
|
|
background: var(--color-error);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
color: var(--color-text-inverse);
|
|
|
|
|
|
border: 1px solid var(--color-error);
|
|
|
|
|
|
box-shadow: 0 1px 0 rgba(0, 0, 0, 0.2), var(--shadow-inset-hi);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
.confirm-dialog-actions .btn-danger:hover:not(:disabled) {
|
|
|
|
|
|
filter: brightness(1.06);
|
|
|
|
|
|
transform: translateY(-1px);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Home page */
|
|
|
|
|
|
.home-page {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
max-width: 52rem;
|
2026-03-19 20:40:51 +00:00
|
|
|
|
margin: 0 auto;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
padding: var(--spacing-2xl) var(--spacing-xl);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
width: 100%;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
gap: var(--spacing-md);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-hero {
|
|
|
|
|
|
text-align: center;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
padding: var(--spacing-sm) 0 var(--spacing-md);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-logo {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
width: 72px;
|
2026-03-19 20:40:51 +00:00
|
|
|
|
height: auto;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
margin: 0 auto;
|
2026-03-19 20:40:51 +00:00
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Home resource bar - prominent */
|
|
|
|
|
|
.home-resource-bar {
|
|
|
|
|
|
width: 100%;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
max-width: 420px;
|
|
|
|
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
|
|
|
|
background: var(--color-surface-raised);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
box-shadow: var(--shadow-subtle);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-resource-bar-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
font-size: var(--text-sm);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
color: var(--color-text-secondary);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
margin-bottom: var(--spacing-sm);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-resource-label {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-weight: var(--font-weight-medium);
|
|
|
|
|
|
color: var(--color-text-primary);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-resource-pct {
|
|
|
|
|
|
margin-left: auto;
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
font-family: var(--font-mono);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-weight: var(--font-weight-medium);
|
|
|
|
|
|
font-size: var(--text-xs);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-resource-track {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 6px;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
border-radius: var(--radius-full);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-resource-fill {
|
|
|
|
|
|
height: 100%;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
border-radius: var(--radius-full);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
transition: width 500ms ease;
|
|
|
|
|
|
}
|
2026-03-31 15:37:58 +00:00
|
|
|
|
.home-cluster-status {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-xs);
|
2026-03-31 15:37:58 +00:00
|
|
|
|
color: var(--color-text-muted);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
margin-top: var(--spacing-sm);
|
2026-03-31 15:37:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-cluster-dot {
|
|
|
|
|
|
width: 6px;
|
|
|
|
|
|
height: 6px;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
border-radius: var(--radius-full);
|
2026-03-31 15:37:58 +00:00
|
|
|
|
background: var(--color-success);
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
}
|
2026-03-19 20:40:51 +00:00
|
|
|
|
|
feat: localai assistant chat modality (#9602)
* fix(tests): inline model_test fixtures after tests/models_fixtures removal
The previous reorg removed tests/models_fixtures/ but core/config/model_test.go
still read CONFIG_FILE/MODELS_PATH env vars pointing into that directory, so
`make test` failed with "open : no such file or directory" on the readConfigFile
spec (the suite ran with --fail-fast and bailed before openresponses_test).
Inline the YAMLs (config/embeddings/grpc/rwkv/whisper) directly into the test
file, materialise them into a per-test tmpdir via BeforeEach, and drop the
env-var lookups. The test no longer depends on Makefile plumbing.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: claude-code:claude-opus-4-7 [Edit] [Write] [Bash]
* refactor(modeladmin): extract model-admin helpers into a service package
Lift the bodies of EditModelEndpoint, PatchConfigEndpoint,
ToggleStateModelEndpoint, TogglePinnedModelEndpoint and
VRAMEstimateEndpoint into core/services/modeladmin so the same logic can
be called by non-HTTP clients (notably the in-process MCP server that
backs the LocalAI Assistant chat modality, landing in a follow-up commit).
The HTTP handlers shrink to thin shells that parse echo inputs, call the
matching helper, map typed errors (ErrNotFound, ErrConflict,
ErrPathNotTrusted, ErrBadAction, ...) to the existing HTTP status codes,
and render the existing response shapes. No REST-surface behaviour change;
the existing localai endpoint tests cover the regression net.
Adds focused unit tests for each helper against tmp-dir-backed
ModelConfigLoader fixtures (deep-merge patch, rename + conflict, path
separator guard, toggle/pin enable/disable, sync callback).
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write] [Bash]
Signed-off-by: Ettore Di Giacinto <[email protected]>
* feat(assistant): LocalAI Assistant chat modality with in-memory MCP server
Adds a chat modality, admin-only, that wires the chat session to an
in-memory MCP server exposing LocalAI's own admin/management surface as
tools. An admin can install models, manage backends, edit configs and
check status by chatting; the LLM calls tools like gallery_search,
install_model, import_model_uri, list_installed_models, edit_model_config
and surfaces the results.
Same Go package powers two modes:
pkg/mcp/localaitools/
NewServer(client, opts) builds an MCP server that registers the
19-tool admin catalog. The LocalAIClient interface has two impls:
- inproc.Client — calls services directly (no HTTP loopback,
no synthetic admin API key). Used in-process by the chat handler.
- httpapi.Client — calls the LocalAI REST API. Used by the new
`local-ai mcp-server --target=…` subcommand to control a remote
LocalAI from a stdio MCP host.
Tools and their embedded skill prompts are agnostic to which client
backs them. Skill prompts are markdown files under prompts/, embedded
via go:embed and assembled into the system prompt at server init.
Wiring:
- core/http/endpoints/mcp/localai_assistant.go — process-wide holder
that spins up the in-memory MCP server once at Application start
using paired net.Pipe transports, then reuses LocalToolExecutor
(no fork) for every chat request that opts in.
- core/http/endpoints/openai/chat.go — small branch ahead of the
existing MCP block: when metadata.localai_assistant=true,
defense-in-depth admin check + executor swap + system-prompt
injection. All downstream tool dispatch is unchanged.
- core/http/auth/{permissions,features}.go — adds
FeatureLocalAIAssistant; gating happens at the chat handler entry
plus admin-only `/api/settings`.
- core/cli/{run.go,cli.go,mcp_server.go} —
LOCALAI_DISABLE_ASSISTANT flag (runtime-toggleable via Settings, no
restart), plus `local-ai mcp-server` stdio subcommand.
- core/config/runtime_settings.go — `localai_assistant_enabled`
runtime setting; the chat handler reads `DisableLocalAIAssistant`
live at request entry.
UI:
- Home.jsx — prominent self-explanatory CTA card on first run
("Manage LocalAI by chatting"); collapses to a compact
"Manage by chat" button in the quick-links row once used,
persisted via localStorage.
- Chat.jsx — admin-only "Manage" toggle in the chat header,
"Manage mode" badge, dedicated empty-state copy, starter chips.
- Settings.jsx — "LocalAI Assistant" section with the runtime
enable toggle.
- useChat.js — `localaiAssistant` flag on the chat schema; injects
`metadata.localai_assistant=true` on requests when active.
Distributed mode: the in-memory MCP server lives only on the head node;
inproc.Client wraps already-distributed-aware services so installs
propagate to workers via the existing GalleryService machinery.
Documentation: `.agents/localai-assistant-mcp.md` is the contributor
contract — when adding an admin REST endpoint, also add a LocalAIClient
method, an inproc + httpapi impl, a tool registration, and a skill
prompt update; the AGENTS.md index links to it.
Out of scope (follow-ups): per-tool RBAC granularity for non-admin
read-only access; streaming mcp_tool_progress for long installs;
React Vitest rig for the UI changes.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write] [Bash]
Signed-off-by: Ettore Di Giacinto <[email protected]>
* refactor(assistant): extract tool/capability/MiB/server-name constants
The MCP tool surface, capability tag set, server-name default, and the
chat-handler metadata key were repeated as bare string literals across
seven files. Renaming any one required hand-editing every call site and
risked code/test/prompt drift.
This pulls them into typed constants:
- pkg/mcp/localaitools/tools.go — Tool* constants for the 19 MCP tools,
plus DefaultServerName.
- pkg/mcp/localaitools/capability.go — typed Capability + constants for
the capability tag set the LLM passes to list_installed_models. The
type rides through LocalAIClient.ListInstalledModels and replaces the
triplet of "embed"/"embedding"/"embeddings" with the single
CapabilityEmbeddings.
- pkg/mcp/localaitools/inproc/client.go — bytesPerMiB constant for the
VRAMEstimate byte→MB conversion.
- core/http/endpoints/mcp/tools.go — MetadataKeyLocalAIAssistant for the
"localai_assistant" request-metadata key consumed by the chat handler.
Tool registrations, the test catalog, the dispatch table, the validation
fixtures, and the fake/stub clients all reference the constants. The
embedded skill prompts under prompts/ keep their bare strings (go:embed
markdown can't import Go constants); the existing TestPromptsContain
SafetyAnchors guards the alignment.
No behaviour change. All tests pass with -race.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write] [Bash]
Signed-off-by: Ettore Di Giacinto <[email protected]>
* refactor(modeladmin): typed Action for ToggleState/TogglePinned
The toggle/pin verbs were bare strings everywhere — handler signatures,
service implementations, MCP tool args, the fake/stub clients, the
inproc and httpapi LocalAIClient impls, plus 4 test files. A typo in
any caller silently fell through to the runtime "must be 'enable' or
'disable'" check.
Introduce core/services/modeladmin.Action (string alias) with
ActionEnable, ActionDisable, ActionPin, ActionUnpin and a small Valid
helper. The compiler now catches mismatches at every boundary; renames
ripple through one source of truth.
LocalAIClient.ToggleModelState/Pinned signatures change to take
modeladmin.Action. The package is brand-new and unreleased so this is
a free public-API tightening.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Bash]
Signed-off-by: Ettore Di Giacinto <[email protected]>
* fix(assistant): respect ctx cancellation on gallery channel sends
InstallModel, DeleteModel, ImportModelURI, InstallBackend and
UpgradeBackend all pushed onto galleryop channels with bare sends. If the
worker was paused or the buffer full, the chat-handler goroutine blocked
forever — the LLM kept polling and the request leaked.
Wrap the five sends in a sendModelOp/sendBackendOp helper that selects
on ctx.Done() so a cancelled chat completion surfaces context.Canceled
back to the LLM instead of hanging.
Adds inproc/client_test.go with a pre-cancelled-ctx regression test on
InstallModel; the helpers are shared so the same guarantee covers the
other four call sites.
Assisted-by: Claude:claude-opus-4-7 [Edit] [Write] [Bash]
Signed-off-by: Ettore Di Giacinto <[email protected]>
* fix(assistant): graceful shutdown for in-memory holder and stdio CLI
Two related leaks:
- Application.start() built the LocalAIAssistantHolder but never wired
Close() into the graceful-termination chain — the in-memory MCP
transport pair stayed alive until process exit, and the goroutines
behind net.Pipe() didn't drain. Hook into the existing
signals.RegisterGracefulTerminationHandler chain (same pattern as
core/http/endpoints/mcp/tools.go:770).
- core/cli/mcp_server.go ran srv.Run with context.Background(); a
Ctrl-C from the host (Claude Desktop, mcphost, npx inspector) or a
SIGTERM from process supervision left the stdio loop reading from a
closed pipe. Switch to signal.NotifyContext to surface the signal
through ctx and let srv.Run drain.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Bash]
Signed-off-by: Ettore Di Giacinto <[email protected]>
* fix(assistant): typed HTTPError + propagate prompt walk error
The httpapi client detected "no such job" by substring-matching on the
error string ("404", "could not find") — brittle to status-code
formatting changes and to LocalAI fixing /models/jobs/:uuid to return a
proper 404. Replace with a typed *HTTPError whose Is() method honours
errors.Is(err, ErrHTTPNotFound). The 500-with-"could not find" branch
stays as a transitional fallback documented in Is().
Same change covers ListNodes' 404 fallback for the /api/nodes endpoint.
Adds httptest tests for both 404 and the legacy 500 path, plus a
direct errors.Is exposure test so external callers (the standalone
stdio CLI host) can match without re-string-parsing.
Also tightens prompts.SystemPrompt: panic when fs.WalkDir on the
embedded FS fails. The only realistic cause is a build-time //go:embed
misconfiguration; serving an empty system prompt to the LLM is much
worse than crashing init. TestSystemPromptIncludesAllEmbeddedFiles
catches regressions in CI.
Assisted-by: Claude:claude-opus-4-7 [Edit] [Write] [Bash]
Signed-off-by: Ettore Di Giacinto <[email protected]>
* fix(modeladmin): atomic writes for model config files
The five sites that wrote model YAML used os.WriteFile, which opens
with O_TRUNC|O_WRONLY|O_CREATE. A crash mid-write left the destination
truncated and the model unloadable until manual repair. Pre-existing
behaviour inherited from the original endpoint handlers — fix once now
that there's a single helper.
Adds writeFileAtomic: writes to a sibling temp file, chmods, syncs via
Close(), then os.Rename. Same-directory temp keeps the rename atomic on
the same filesystem; cleanup runs on every error path so stray temps
don't accumulate. No new dependency.
Applied to:
- ConfigService.PatchConfig
- ConfigService.EditYAML (both rename and in-place branches)
- mutateYAMLBoolFlag (drives ToggleState + TogglePinned)
atomic_test.go covers the happy path plus a read-only-dir failure case
that asserts the original file is preserved (skipped on Windows where
the chmod trick is POSIX-specific).
Assisted-by: Claude:claude-opus-4-7 [Edit] [Write] [Bash]
Signed-off-by: Ettore Di Giacinto <[email protected]>
* chore(assistant): prune dead code, mark stub, document conventions
Three small cleanups landing together:
- Drop the unused errNotImplemented sentinel from inproc/client.go.
All five methods that used to return it are wired to modeladmin
helpers since the Phase B commit; the package var is dead.
- Annotate httpapi.Client.GetModelConfig as a known stub. LocalAI's
/models/edit/:name returns rendered HTML, not JSON, so the standalone
CLI's get_model_config tool surfaces a clear error to the LLM. A
future JSON-only /api/models/config-yaml/:name endpoint is tracked in
the agent contract; FIXME points at it.
- Extend `.agents/localai-assistant-mcp.md` with a "Code conventions"
section that documents the audit-driven rules: tool/Capability/Action
constants, errors.Is over substring matching, ctx-aware channel
sends, atomic writes, and graceful shutdown. Refresh the file map so
it lists tools.go and capability.go and drops the removed
tools_bootstrap.go.
The tools_models.go diff is a comment-only change explaining why the
ModelName empty-string check stays at the tool layer (consistency
across LocalAIClient implementations, since the SDK schema validator
only enforces presence, not non-empty).
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Bash]
Signed-off-by: Ettore Di Giacinto <[email protected]>
* test(assistant): convert test files to ginkgo + gomega
The repo convention (per core/http/endpoints/localai/*_test.go,
core/gallery/**, etc.) is Ginkgo v2 with Gomega assertions. The tests I
introduced for the assistant feature used vanilla testing.T, which made
them stand out and stripped the BDD structure the rest of the suite
relies on.
Convert every test file in the assistant scope to Ginkgo:
pkg/mcp/localaitools/
dto_test.go — Describe("DTOs round-trip through JSON")
prompts_test.go — Describe("SystemPrompt assembler")
server_test.go — Describe("Server tool catalog"),
Describe("Tool dispatch"),
Describe("Tool error surfacing"),
Describe("Argument validation"),
Describe("Concurrent tool calls")
parity_test.go — Describe("LocalAIClient parity"),
hosts the suite's single RunSpecs (the file
is package localaitools_test so it can
import httpapi without an import cycle;
Ginkgo aggregates Describes from both the
internal and external test packages into
one run).
httpapi/client_test.go — Describe("httpapi.Client against the
LocalAI admin REST surface"),
Describe("ErrHTTPNotFound"),
Describe("Bearer token")
inproc/client_test.go — Describe("inproc.Client cancellation")
core/services/modeladmin/
config_test.go — Describe("ConfigService") with sub-Describes
for GetConfig, PatchConfig, EditYAML
state_test.go — Describe("ConfigService.ToggleState")
pinned_test.go — Describe("ConfigService.TogglePinned")
atomic_test.go — Describe("writeFileAtomic")
core/http/endpoints/mcp/
localai_assistant_test.go — Describe("LocalAIAssistantHolder")
Each package gets a `*_suite_test.go` with the standard
`RegisterFailHandler(Fail) + RunSpecs(t, "...")` boilerplate. Helpers
that previously took *testing.T (newTestService, writeModelYAML,
readMap, sortedStrings, sortGalleries, etc.) drop the *T receiver and
use Gomega Expectations directly. tmp dirs come from GinkgoT().TempDir().
No semantic change to test coverage — every original assertion has a
direct Gomega counterpart. All suites pass with -race.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write] [Bash]
Signed-off-by: Ettore Di Giacinto <[email protected]>
* test+docs(assistant): drift detector for Tool ↔ REST route mapping
Honest gap from the audit: the parity_test.go suite only checks four
methods, and uses the same httpapi.Client for both sides — it asserts
stability of the DTO shapes, not equivalence between in-process and
HTTP. If a contributor adds an admin REST endpoint without an MCP tool,
or a tool without a matching httpapi route, both surfaces silently
diverge.
Add a coverage test plus stronger docs:
- pkg/mcp/localaitools/coverage_test.go introduces a hand-maintained
toolToHTTPRoute map: every Tool* constant must list the REST endpoint
the httpapi.Client hits (or "(none)" with a documented reason). Two
Ginkgo specs assert the map and the published catalog stay in sync —
one fails when a Tool is added without a route entry, the other fails
when a route entry references a tool that no longer exists. Verified
by removing the ToolDeleteModel entry locally; the test fired with a
clear message pointing the contributor at the file.
Deliberate non-test: we don't enumerate live admin REST routes from
here. Walking the route registry requires booting Application;
parsing core/http/routes/localai.go is brittle. The "new admin REST
endpoint → MCP tool" direction stays a PR checklist item — see below.
- AGENTS.md gets a new Quick Reference bullet that calls out the rule
and points at the test by name.
- .agents/api-endpoints-and-auth.md tightens the existing "Companion:
MCP admin tool surface" subsection from "if useful, consider..." to
"MUST be considered, with three concrete outcomes (tool added,
deliberately skipped with documented reason, or forgot — which
breaks the contract)". Adds a checklist item at the bottom of the
file's authoritative checklist.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write] [Bash]
Signed-off-by: Ettore Di Giacinto <[email protected]>
* refactor(assistant): drop duplicate DTOs, surface canonical types
Audit feedback: localaitools/dto.go reinvented several types that already
existed in the codebase. Replace the duplicates with the canonical types
so the LLM-visible wire format stays aligned with the rest of LocalAI by
construction (no parallel structs to keep in sync).
Removed (and the canonical type now used by the LocalAIClient interface):
localaitools.Gallery → config.Gallery
localaitools.GalleryModelHit → gallery.Metadata
localaitools.VRAMEstimate → vram.EstimateResult
Tightened scope:
localaitools.Backend → kept, but reduced to {Name, Installed}.
ListKnownBackends now returns
[]schema.KnownBackend (the canonical
type already used by REST /backends/known).
Kept with documented rationale:
localaitools.JobStatus — galleryop.OpStatus has Error error which
marshals to "{}". JobStatus is the
JSON-friendly mirror.
localaitools.Node — nodes.BackendNode carries gorm internals
+ token hash; we expose only the
LLM-relevant fields.
ImportModelURIRequest/Response — schema.ImportModelRequest and
GalleryResponse are wire-shaped, mine
are LLM-shaped (BackendPreference flat,
AmbiguousBackend exposed).
Side wins:
- Drop bytesPerMiB; vram.EstimateResult already carries human-readable
display strings (size_display, vram_display) the LLM uses directly.
- Drop the handler-private vramEstimateRequest in
core/http/endpoints/localai/vram.go and bind directly into
modeladmin.VRAMRequest (now JSON-tagged).
Both clients pass through these types now where possible (e.g.
ListGalleries in inproc.Client is a one-liner returning
AppConfig.Galleries; httpapi.Client.GallerySearch decodes straight into
[]gallery.Metadata).
All tests green with -race.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Bash]
Signed-off-by: Ettore Di Giacinto <[email protected]>
* refactor(assistant): extract REST route paths into named constants
httpapi.Client had 18 bare-string path sites scattered across methods.
Pull them into pkg/mcp/localaitools/httpapi/routes.go: static paths as
package-private constants, dynamic paths as small builders that handle
url.PathEscape on segment values.
No behaviour change. Drops the now-unused net/url import from client.go
since path escaping moved into routes.go alongside the path it applies to.
Local-only by design: the server-side registrations in
core/http/routes/localai.go remain bare strings. Sharing constants across
the pkg/ ↔ core/ boundary would invert the layering today; the existing
Tool↔REST drift-detector in coverage_test.go is the safety net for that
direction.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7 [Claude Code]
* docs(assistant): align with shipped UI and dropped bootstrap env vars
The LocalAI Assistant doc still described the older iteration:
- The in-chat toggle was renamed from "Admin" to "Manage" (the badge is
now "Manage mode" and the home page exposes a "Manage by chat" CTA).
- LOCALAI_ASSISTANT_BOOTSTRAP_MODEL / --localai-assistant-bootstrap-model
and the bootstrap_default_model tool were removed — admins pick a model
from the existing selector instead, no env-var configuration required.
- The shipped tool catalog includes import_model_uri but didn't appear in
the doc; bootstrap_default_model appeared but no longer exists.
- The Settings → LocalAI Assistant runtime toggle wasn't mentioned as the
preferred way to disable without restart.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude:claude-opus-4-7 [Claude Code]
---------
Signed-off-by: Ettore Di Giacinto <[email protected]>
2026-04-28 17:29:27 +00:00
|
|
|
|
/* Home assistant CTA — a self-explanatory entry point for the in-process
|
|
|
|
|
|
admin tool surface. Distinct from the chat composer below it; uses the
|
|
|
|
|
|
accent token + a subtle gradient so it reads as a primary action without
|
|
|
|
|
|
looking AI-slop generative. */
|
|
|
|
|
|
.home-assistant-card {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
|
|
|
|
background: var(--color-surface-raised);
|
|
|
|
|
|
border: 1px solid var(--color-accent);
|
|
|
|
|
|
border-radius: var(--radius-xl);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
color: var(--color-text);
|
|
|
|
|
|
transition: background-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-assistant-card:hover {
|
|
|
|
|
|
background: var(--color-accent-light, var(--color-surface-hover));
|
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
|
box-shadow: var(--shadow-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-assistant-card:active {
|
|
|
|
|
|
transform: translateY(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-assistant-icon {
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
width: 40px;
|
|
|
|
|
|
height: 40px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: var(--color-accent);
|
|
|
|
|
|
color: var(--color-on-accent, #ffffff);
|
|
|
|
|
|
font-size: 1.1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-assistant-text {
|
|
|
|
|
|
flex: 1 1 auto;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-assistant-title {
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
font-size: 1rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-assistant-desc {
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-assistant-cta {
|
|
|
|
|
|
flex: 0 0 auto;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
color: var(--color-accent);
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (max-width: 600px) {
|
|
|
|
|
|
.home-assistant-card {
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-assistant-cta {
|
|
|
|
|
|
flex-basis: 100%;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 20:40:51 +00:00
|
|
|
|
/* Home chat card */
|
|
|
|
|
|
.home-chat-card {
|
|
|
|
|
|
width: 100%;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
background: var(--color-surface-raised);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
border-radius: var(--radius-xl);
|
|
|
|
|
|
padding: var(--spacing-lg);
|
|
|
|
|
|
box-shadow: var(--shadow-md);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-model-row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
margin-bottom: var(--spacing-md);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-file-tags {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
margin-bottom: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-file-tag {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-full);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-xs);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-file-tag button {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
font-size: 0.625rem;
|
|
|
|
|
|
}
|
2026-04-14 21:53:03 +00:00
|
|
|
|
.home-file-tag button:hover {
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
}
|
2026-03-19 20:40:51 +00:00
|
|
|
|
|
|
|
|
|
|
/* Home input container */
|
|
|
|
|
|
.home-input-container {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
background: var(--color-surface-sunken);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
border: 1px solid var(--color-border-default);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
transition: border-color var(--duration-fast), box-shadow var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-input-container:focus-within {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
border-color: var(--color-primary);
|
|
|
|
|
|
box-shadow: 0 0 0 3px var(--color-primary-light);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-textarea {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
font-size: var(--text-base);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
resize: none;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
min-height: 84px;
|
|
|
|
|
|
line-height: var(--leading-normal);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-textarea::placeholder {
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-input-footer {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: var(--spacing-xs) var(--spacing-sm) var(--spacing-xs) var(--spacing-md);
|
|
|
|
|
|
border-top: 1px solid var(--color-border-divider);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-attach-buttons {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-attach-btn {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
padding: 6px 8px;
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
transition: color var(--duration-fast), background var(--duration-fast);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-attach-btn:hover {
|
|
|
|
|
|
color: var(--color-primary);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
background: var(--color-primary-light);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-input-hint {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
text-align: center;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-xs);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
color: var(--color-text-muted);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
letter-spacing: 0.02em;
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-send-btn {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
width: 34px;
|
|
|
|
|
|
height: 34px;
|
2026-03-19 20:40:51 +00:00
|
|
|
|
background: var(--color-primary);
|
|
|
|
|
|
color: var(--color-primary-text);
|
|
|
|
|
|
border: none;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
font-size: var(--text-sm);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
cursor: pointer;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
box-shadow: var(--shadow-sm);
|
|
|
|
|
|
transition: background var(--duration-fast), transform 100ms, box-shadow var(--duration-fast);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-send-btn:hover:not(:disabled) {
|
|
|
|
|
|
background: var(--color-primary-hover);
|
|
|
|
|
|
transform: scale(1.05);
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-send-btn:disabled {
|
|
|
|
|
|
opacity: 0.3;
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-send-btn:active:not(:disabled) {
|
|
|
|
|
|
transform: scale(0.92);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Home quick links */
|
|
|
|
|
|
.home-quick-links {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-link-btn {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
padding: 8px var(--spacing-md);
|
|
|
|
|
|
background: var(--color-surface-raised);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-full);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: var(--font-weight-medium);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
text-decoration: none;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
box-shadow: var(--shadow-subtle);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
transition: all var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-link-btn:hover {
|
|
|
|
|
|
border-color: var(--color-primary-border);
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
transform: translateY(-1px);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
box-shadow: var(--shadow-sm);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Home loaded models */
|
|
|
|
|
|
.home-loaded-models {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
align-items: center;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
background: var(--color-surface-raised);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-xs);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
width: 100%;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
box-shadow: var(--shadow-subtle);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-loaded-dot {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
width: 8px;
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
border-radius: var(--radius-full);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
background: var(--color-success);
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-loaded-text {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-weight: var(--font-weight-medium);
|
|
|
|
|
|
color: var(--color-text-primary);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-loaded-list {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-loaded-item {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
2026-04-14 21:53:03 +00:00
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 3px 10px;
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
border: 1px solid var(--color-border-divider);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
border-radius: var(--radius-full);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-xs);
|
feat(react-ui): editorial refresh with Nord palette and polished primitives (#9550)
* feat(react-ui): editorial refresh with Nord palette and polished primitives
Replaces the cool gray-blue theme with a deep Nord-inspired palette:
frost-cyan accent (#88c0d0) on deep blue-black surfaces (#13171f /
#1a1f2a / #242a36), snow-storm text scale, aurora status colours.
- Typography: Geist Variable + Geist Mono Variable (Google Fonts) with
ss01/ss03/cv11 stylistic alternates; strengthened h1-h6 hierarchy;
editorial negative tracking.
- Primitives: buttons gain depth (inset highlight + hover lift +
brightness filter); inputs become sunken wells with sage-swap-to-frost
focus rings; cards hover-lift and gain an .card--accent left-rail
variant; badges become mono caps rectangles with tabular-nums.
- Chrome: sidebar active state is now an inset left rail + tint
(no border-left); modals get popIn animation and proper shadow lift;
toasts carry an inset accent bar + slide-in instead of tinted fills;
operations bar breathes on active installs.
- Empty states: editorial pattern (eyebrow rule, large mono title,
52ch lede) that inherits gracefully even without page JSX edits.
- Chat: assistant bubbles drop the gray-nested-in-gray card for a
transparent pull-quote with a left border; user bubbles soften from
loud accent fill to a subtle frost tint.
- Motion: custom spring easing cubic-bezier(0.22,1,0.36,1), 180ms
standard; breathing/pulse/popIn keyframes; global prefers-reduced-
motion honoring.
- Radii tightened to 3/5/8/10px; warm-shadow tokens redone for cool
depth; ::selection, :focus-visible, kbd globals added.
- Migrated hardcoded 'JetBrains Mono' CSS literals to var(--font-mono)
so the Geist Mono swap lands everywhere.
Scope is intentionally tokens + primitives only. Page JSX and the
~1,800 inline style={{…}} instances are untouched and flagged as
follow-ups.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
* feat(react-ui): complete-coverage pass — migrate inline styles to tokens
Follows up the editorial/Nord token refresh with a mechanical sweep of
page JSX and shared components so nothing bypasses the design system.
- Font family: replaced 80+ 'JetBrains Mono' / 'Space Grotesk' inline
literals (and the string-CSS variants in CollectionDetails and
AgentStatus) with var(--font-mono) / var(--font-sans). SVG <text>
nodes that used the attribute form were switched to style={{ }} so
the CSS variable resolves.
- Radii: every unquoted numeric borderRadius (2/3/4/10) is now a
var(--radius-*) token; 50% and 999px kept as computed shapes.
- Spacing: clean-token gaps and margins (4/8/16px) moved to
var(--spacing-xs/sm/md); padding: '4px 8px' and '8px 16px' lifted
into token pairs. Micro-values (2/6/10/12px) left inline where no
token maps cleanly.
- Colors: Talk.jsx button/canvas-surface hardcodes moved to
var(--color-*); FineTune.jsx chart series colours now use the
--color-data-* Nord palette (cyan/red/purple/orange instead of
tailwind hex); AgentStatus tool-call icon and error tag hex swapped
for var(--color-warning) / var(--color-text-inverse).
- CodeMirror editor (utils/cmTheme.js): both themes rebased on Nord —
polar-night surfaces and aurora syntax highlighting (dark), snow-
storm surfaces with darkened aurora (light). Caret/selection/active
line/search now frost-cyan tinted instead of legacy indigo/purple.
Legitimately dynamic styles (computed widths, per-row colours, canvas
2D context fill/stroke for waveform and spectrogram drawing) remain
inline — they can't be expressed as CSS tokens.
29 files, +237/-237 — identity preserved, semantics re-anchored to
the token system.
Assisted-by: Claude:claude-opus-4-7 [Read] [Edit] [Write]
2026-04-24 21:35:59 +00:00
|
|
|
|
font-family: var(--font-mono);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-loaded-item button {
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
font-size: 0.625rem;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-stop-all {
|
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
background: none;
|
|
|
|
|
|
border: 1px solid var(--color-error);
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
font-size: 0.75rem;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Home wizard (no models) */
|
|
|
|
|
|
.home-wizard {
|
|
|
|
|
|
max-width: 48rem;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-wizard-hero {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
padding: var(--spacing-xl) 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-wizard-hero h1 {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-2xl);
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
letter-spacing: -0.015em;
|
2026-03-19 20:40:51 +00:00
|
|
|
|
margin-bottom: var(--spacing-sm);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
color: var(--color-text-primary);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-wizard-hero p {
|
|
|
|
|
|
color: var(--color-text-secondary);
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-base);
|
|
|
|
|
|
line-height: var(--leading-normal);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
.home-wizard-steps {
|
|
|
|
|
|
margin-bottom: var(--spacing-xl);
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-wizard-steps h2 {
|
2026-04-14 21:53:03 +00:00
|
|
|
|
font-size: var(--text-lg);
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
2026-03-19 20:40:51 +00:00
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-wizard-step {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
padding: var(--spacing-sm) 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-wizard-step-num {
|
|
|
|
|
|
width: 28px;
|
|
|
|
|
|
height: 28px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: var(--color-primary);
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-wizard-step strong {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-wizard-step p {
|
|
|
|
|
|
font-size: 0.8125rem;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.home-wizard-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: add biometrics UI (#9524)
* feat(react-ui): add Face & Voice Recognition pages
Expose the face and voice biometrics endpoints
(/v1/face/*, /v1/voice/*) through the React UI. Each page has four
tabs driving the six endpoints per modality: Analyze (demographics
with bounding boxes / waveform segments), Compare (verify with a
match gauge and live threshold slider), Enrollment (register /
identify / forget with a top-K matches view), Embedding (raw
vector inspector with sparkline + copy).
MediaInput supports file upload plus live capture: webcam
snap-to-canvas for face, MediaRecorder -> AudioContext ->
16-bit PCM mono WAV transcode for voice (libsndfile on the
backend only handles WAV/FLAC/OGG natively).
Sidebar gets a new Biometrics section feature-gated on
face_recognition / voice_recognition; routes are wrapped in
<RequireFeature>. No new dependencies -- Font Awesome icons
picked from the Free set.
Assisted-by: Claude:Opus 4.7
* fix(localai): accept data URI prefixes with codec/charset params
Browser MediaRecorder produces data URIs like
data:audio/webm;codecs=opus;base64,...
so the pre-';base64,' section can carry multiple parameter
segments. The `^data:([^;]+);base64,` regex in pkg/utils/base64.go
and core/http/endpoints/localai/audio.go only matched exactly one
segment, so recordings straight from the React UI's live-capture
tab failed the strip and then tripped the base64 decoder on the
leading 'data:' literal, surfacing as
"invalid audio base64: illegal base64 data at input byte 4"
Widened both regexes to `^data:[^,]+?;base64,` so any number of
';param=value' segments between the mime type and ';base64,' are
tolerated. Added a regression test covering the MediaRecorder
shape.
Assisted-by: Claude:Opus 4.7
* fix(insightface): scope pack ONNX loading to known manifests
LocalAI's gallery extracts buffalo_* zips flat into the models
directory, which inevitably mixes with ONNX files from other
backends (opencv face engine, MiniFASNet antispoof, WeSpeaker
voice embedding) and older buffalo pack installs. Feeding those
foreign files into insightface's model_zoo.get_model() blows up
inside the router -- it assumes a 4-D NCHW input and indexes
`input_shape[2]` on tensors that aren't shaped like a face model,
raising IndexError mid-load and leaving the backend unusable.
The router's dispatch isn't amenable to per-file try/except alone
(first-file-wins picks det_10g.onnx from buffalo_l even when the
user asked for buffalo_sc -- alphabetical order happens to favour
the wrong pack). Instead, ship an explicit manifest of the
upstream v0.7 pack contents and scope the glob to that when the
requested pack is known. The manifest is small and stable; future
packs can be added alongside or fall through to the tolerance
loop, which also swallows any remaining IndexError / ValueError
from foreign files with a clear `[insightface] skipped` stderr
line for diagnostics.
Assisted-by: Claude:Opus 4.7
* fix(speaker-recognition): extract FBank features for rank-3 ONNX encoders
Pre-exported speaker-encoder ONNX graphs come in two shapes:
rank-2 [batch, samples] -- some 3D-Speaker exports,
take raw waveform directly.
rank-3 [batch, frames, n_mels] -- WeSpeaker and most Kaldi-
lineage encoders, expect
pre-computed Kaldi FBank.
OnnxDirectEngine unconditionally fed `audio.reshape(1, -1)` --
correct for rank-2, IndexError-on-input_shape[3] on rank-3, which
surfaced to the UI as
"Invalid rank for input: feats Got: 2 Expected: 3"
Detect the input rank at session init and run Kaldi FBank
(80-dim, 25ms/10ms frames, dither=0.0, per-utterance CMN) before
the forward pass when rank>=3. All knobs are configurable via
backend options for encoders that deviate from defaults.
torchaudio.compliance.kaldi is already in the backend's
requirements (SpeechBrain pulls torchaudio in), so no new
dependency.
Assisted-by: Claude:Opus 4.7
* fix(biometrics): isolate face and voice vector stores
Face (ArcFace, 512-D) and voice (ECAPA-TDNN 192-D / WeSpeaker
256-D) biometric embeddings were colliding inside a single
in-memory local-store instance. Enrolling one after the other
failed with
"Try to add key with length N when existing length is M"
because local-store correctly refuses to mix dimensions in one
keyspace.
The registries were constructed with `storeName=""`, which in
StoreBackend() is just a WithModel() call. But ModelLoader's
cache is keyed on `modelID`, not `model` -- so both registries
collapsed to the same `modelID=""` slot and reused the same
backend process despite looking isolated on paper.
Three complementary fixes:
1. application.go -- give each registry a distinct default
namespace ("localai-face-biometrics" /
"localai-voice-biometrics"). The comment claimed
isolation, now it's actually enforced.
2. stores.go -- pass the storeName as both WithModelID and
WithModel so the ModelLoader cache key separates
namespaces and the loader spawns distinct processes.
3. local-store/store.go -- drop the Load() `opts.Model != ""`
guard. It was there to prevent generic model-loading loops
from picking up local-store by accident, but that auto-load
path is being retired; the guard now just blocks legitimate
namespace isolation. opts.Model is treated as a tag; the
per-tuple process isolation upstream handles discrimination.
Assisted-by: Claude:Opus 4.7
* fix(gallery): stale-file cleanup and upgrade-tmp directory safety
Two related robustness fixes for backend install/upgrade:
pkg/downloader/uri.go
OCI downloads passed through
if filepath.Ext(filePath) != "" ...
filePath = filepath.Dir(filePath)
which was intended to redirect file-shaped download targets
into their parent directory for OCI extraction. The heuristic
misfires on directory-shaped paths with a dot-suffix --
gallery.UpgradeBackend uses
tmpPath = "<backendsPath>/<name>.upgrade-tmp"
and Go's filepath.Ext treats ".upgrade-tmp" as an extension.
The rewrite landed the extraction at "<backendsPath>/", which
then **overwrote the real install** (backends/<name>/) with a
flat-layout file and left a stray run.sh at the top level. The
tmp dir itself stayed empty, so the validation step that
checked "<tmpPath>/run.sh" predictably failed with
"upgrade validation failed: run.sh not found in new backend"
Every manual upgrade silently corrupted the backends tree this
way. Guard the rewrite behind "target isn't already an existing
directory" -- InstallBackend / UpgradeBackend both pre-create
the target as a directory, so they get the correct behaviour;
existing file-path callers with a genuine dot-extension still
get the parent redirect.
core/gallery/backends.go
InstallBackend's MkdirAll returned ENOTDIR when something at
the target path was already a file (legacy dev builds dropped
golang backend binaries directly at `<backendsPath>/<name>`
instead of nesting them under their own subdir). That
permanently blocked reinstall and upgrade for anyone carrying
that state, since every retry hit the same error. Detect a
pre-existing non-directory, warn, and remove it before the
MkdirAll so the fresh install can write the correct nested
layout with metadata.json + run.sh.
Assisted-by: Claude:Opus 4.7
* fix(galleryop): refresh upgrade cache after backend ops
UpgradeChecker caches the last upgrade-check result and only
refreshes on the 6-hour tick or after an auto-upgrade cycle.
Manual upgrades (POST /api/backends/upgrade/:name) go through
the async galleryop worker, which completes the upgrade
correctly but never tells UpgradeChecker to re-check -- so
/api/backends/upgrades continued to list a just-upgraded backend
as upgradeable, indistinguishable from a failed upgrade, for up
to six hours.
Add an optional `OnBackendOpCompleted func()` hook on
GalleryService that fires after every successful install /
upgrade / delete on the backend channel (async, so a slow
callback doesn't stall the queue). startup.go wires it to
UpgradeChecker.TriggerCheck after both services exist. Result:
the upgrade banner clears within milliseconds of the worker
finishing.
Assisted-by: Claude:Opus 4.7
* build: prepend GOPATH/bin to PATH for protogen-go
install-go-tools runs `go install` for protoc-gen-go and
protoc-gen-go-grpc, which writes them into `go env GOPATH`/bin.
That directory isn't on every dev's PATH, and protoc resolves
its code-gen plugins via PATH, so the immediately-following
protoc invocation fails with
"protoc-gen-go: program not found"
which in turn blocks `make build` and any
`make backends/%` target that depends on build.
Prepend `go env GOPATH`/bin to PATH for the protoc invocation
so the freshly-installed plugins are found without requiring a
shell-profile change.
Assisted-by: Claude:Opus 4.7
* refactor(ui-api): non-blocking backend upgrade handler with opcache
POST /api/backends/upgrade/:name used to send the ManagementOp
directly onto the unbuffered BackendGalleryChannel, which blocked
the HTTP request whenever the galleryop worker was busy with a
prior operation. The op also didn't show up in /api/operations,
so the Backends UI couldn't reflect upgrade progress on the
affected row.
Register the op in opcache immediately, wrap it in a cancellable
context, store the cancellation function on the GalleryService,
and push onto the channel from a goroutine so the handler
returns right away. Response gains a `jobID` field and a
`message` string so clients have a consistent handle regardless
of whether the op is queued or running.
Pairs with the OnBackendOpCompleted hook added in the galleryop
commit — together the UI sees the upgrade start, watches
progress via /api/operations, and drops the "upgradeable" flag
the moment the worker finishes.
Assisted-by: Claude:Opus 4.7
2026-04-24 06:50:34 +00:00
|
|
|
|
/* ──────────────────── Biometrics (face + voice recognition) ──────────────────── */
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-page {
|
|
|
|
|
|
padding: var(--spacing-xl);
|
2026-04-27 11:51:29 +00:00
|
|
|
|
max-width: var(--page-max-wide);
|
feat: add biometrics UI (#9524)
* feat(react-ui): add Face & Voice Recognition pages
Expose the face and voice biometrics endpoints
(/v1/face/*, /v1/voice/*) through the React UI. Each page has four
tabs driving the six endpoints per modality: Analyze (demographics
with bounding boxes / waveform segments), Compare (verify with a
match gauge and live threshold slider), Enrollment (register /
identify / forget with a top-K matches view), Embedding (raw
vector inspector with sparkline + copy).
MediaInput supports file upload plus live capture: webcam
snap-to-canvas for face, MediaRecorder -> AudioContext ->
16-bit PCM mono WAV transcode for voice (libsndfile on the
backend only handles WAV/FLAC/OGG natively).
Sidebar gets a new Biometrics section feature-gated on
face_recognition / voice_recognition; routes are wrapped in
<RequireFeature>. No new dependencies -- Font Awesome icons
picked from the Free set.
Assisted-by: Claude:Opus 4.7
* fix(localai): accept data URI prefixes with codec/charset params
Browser MediaRecorder produces data URIs like
data:audio/webm;codecs=opus;base64,...
so the pre-';base64,' section can carry multiple parameter
segments. The `^data:([^;]+);base64,` regex in pkg/utils/base64.go
and core/http/endpoints/localai/audio.go only matched exactly one
segment, so recordings straight from the React UI's live-capture
tab failed the strip and then tripped the base64 decoder on the
leading 'data:' literal, surfacing as
"invalid audio base64: illegal base64 data at input byte 4"
Widened both regexes to `^data:[^,]+?;base64,` so any number of
';param=value' segments between the mime type and ';base64,' are
tolerated. Added a regression test covering the MediaRecorder
shape.
Assisted-by: Claude:Opus 4.7
* fix(insightface): scope pack ONNX loading to known manifests
LocalAI's gallery extracts buffalo_* zips flat into the models
directory, which inevitably mixes with ONNX files from other
backends (opencv face engine, MiniFASNet antispoof, WeSpeaker
voice embedding) and older buffalo pack installs. Feeding those
foreign files into insightface's model_zoo.get_model() blows up
inside the router -- it assumes a 4-D NCHW input and indexes
`input_shape[2]` on tensors that aren't shaped like a face model,
raising IndexError mid-load and leaving the backend unusable.
The router's dispatch isn't amenable to per-file try/except alone
(first-file-wins picks det_10g.onnx from buffalo_l even when the
user asked for buffalo_sc -- alphabetical order happens to favour
the wrong pack). Instead, ship an explicit manifest of the
upstream v0.7 pack contents and scope the glob to that when the
requested pack is known. The manifest is small and stable; future
packs can be added alongside or fall through to the tolerance
loop, which also swallows any remaining IndexError / ValueError
from foreign files with a clear `[insightface] skipped` stderr
line for diagnostics.
Assisted-by: Claude:Opus 4.7
* fix(speaker-recognition): extract FBank features for rank-3 ONNX encoders
Pre-exported speaker-encoder ONNX graphs come in two shapes:
rank-2 [batch, samples] -- some 3D-Speaker exports,
take raw waveform directly.
rank-3 [batch, frames, n_mels] -- WeSpeaker and most Kaldi-
lineage encoders, expect
pre-computed Kaldi FBank.
OnnxDirectEngine unconditionally fed `audio.reshape(1, -1)` --
correct for rank-2, IndexError-on-input_shape[3] on rank-3, which
surfaced to the UI as
"Invalid rank for input: feats Got: 2 Expected: 3"
Detect the input rank at session init and run Kaldi FBank
(80-dim, 25ms/10ms frames, dither=0.0, per-utterance CMN) before
the forward pass when rank>=3. All knobs are configurable via
backend options for encoders that deviate from defaults.
torchaudio.compliance.kaldi is already in the backend's
requirements (SpeechBrain pulls torchaudio in), so no new
dependency.
Assisted-by: Claude:Opus 4.7
* fix(biometrics): isolate face and voice vector stores
Face (ArcFace, 512-D) and voice (ECAPA-TDNN 192-D / WeSpeaker
256-D) biometric embeddings were colliding inside a single
in-memory local-store instance. Enrolling one after the other
failed with
"Try to add key with length N when existing length is M"
because local-store correctly refuses to mix dimensions in one
keyspace.
The registries were constructed with `storeName=""`, which in
StoreBackend() is just a WithModel() call. But ModelLoader's
cache is keyed on `modelID`, not `model` -- so both registries
collapsed to the same `modelID=""` slot and reused the same
backend process despite looking isolated on paper.
Three complementary fixes:
1. application.go -- give each registry a distinct default
namespace ("localai-face-biometrics" /
"localai-voice-biometrics"). The comment claimed
isolation, now it's actually enforced.
2. stores.go -- pass the storeName as both WithModelID and
WithModel so the ModelLoader cache key separates
namespaces and the loader spawns distinct processes.
3. local-store/store.go -- drop the Load() `opts.Model != ""`
guard. It was there to prevent generic model-loading loops
from picking up local-store by accident, but that auto-load
path is being retired; the guard now just blocks legitimate
namespace isolation. opts.Model is treated as a tag; the
per-tuple process isolation upstream handles discrimination.
Assisted-by: Claude:Opus 4.7
* fix(gallery): stale-file cleanup and upgrade-tmp directory safety
Two related robustness fixes for backend install/upgrade:
pkg/downloader/uri.go
OCI downloads passed through
if filepath.Ext(filePath) != "" ...
filePath = filepath.Dir(filePath)
which was intended to redirect file-shaped download targets
into their parent directory for OCI extraction. The heuristic
misfires on directory-shaped paths with a dot-suffix --
gallery.UpgradeBackend uses
tmpPath = "<backendsPath>/<name>.upgrade-tmp"
and Go's filepath.Ext treats ".upgrade-tmp" as an extension.
The rewrite landed the extraction at "<backendsPath>/", which
then **overwrote the real install** (backends/<name>/) with a
flat-layout file and left a stray run.sh at the top level. The
tmp dir itself stayed empty, so the validation step that
checked "<tmpPath>/run.sh" predictably failed with
"upgrade validation failed: run.sh not found in new backend"
Every manual upgrade silently corrupted the backends tree this
way. Guard the rewrite behind "target isn't already an existing
directory" -- InstallBackend / UpgradeBackend both pre-create
the target as a directory, so they get the correct behaviour;
existing file-path callers with a genuine dot-extension still
get the parent redirect.
core/gallery/backends.go
InstallBackend's MkdirAll returned ENOTDIR when something at
the target path was already a file (legacy dev builds dropped
golang backend binaries directly at `<backendsPath>/<name>`
instead of nesting them under their own subdir). That
permanently blocked reinstall and upgrade for anyone carrying
that state, since every retry hit the same error. Detect a
pre-existing non-directory, warn, and remove it before the
MkdirAll so the fresh install can write the correct nested
layout with metadata.json + run.sh.
Assisted-by: Claude:Opus 4.7
* fix(galleryop): refresh upgrade cache after backend ops
UpgradeChecker caches the last upgrade-check result and only
refreshes on the 6-hour tick or after an auto-upgrade cycle.
Manual upgrades (POST /api/backends/upgrade/:name) go through
the async galleryop worker, which completes the upgrade
correctly but never tells UpgradeChecker to re-check -- so
/api/backends/upgrades continued to list a just-upgraded backend
as upgradeable, indistinguishable from a failed upgrade, for up
to six hours.
Add an optional `OnBackendOpCompleted func()` hook on
GalleryService that fires after every successful install /
upgrade / delete on the backend channel (async, so a slow
callback doesn't stall the queue). startup.go wires it to
UpgradeChecker.TriggerCheck after both services exist. Result:
the upgrade banner clears within milliseconds of the worker
finishing.
Assisted-by: Claude:Opus 4.7
* build: prepend GOPATH/bin to PATH for protogen-go
install-go-tools runs `go install` for protoc-gen-go and
protoc-gen-go-grpc, which writes them into `go env GOPATH`/bin.
That directory isn't on every dev's PATH, and protoc resolves
its code-gen plugins via PATH, so the immediately-following
protoc invocation fails with
"protoc-gen-go: program not found"
which in turn blocks `make build` and any
`make backends/%` target that depends on build.
Prepend `go env GOPATH`/bin to PATH for the protoc invocation
so the freshly-installed plugins are found without requiring a
shell-profile change.
Assisted-by: Claude:Opus 4.7
* refactor(ui-api): non-blocking backend upgrade handler with opcache
POST /api/backends/upgrade/:name used to send the ManagementOp
directly onto the unbuffered BackendGalleryChannel, which blocked
the HTTP request whenever the galleryop worker was busy with a
prior operation. The op also didn't show up in /api/operations,
so the Backends UI couldn't reflect upgrade progress on the
affected row.
Register the op in opcache immediately, wrap it in a cancellable
context, store the cancellation function on the GalleryService,
and push onto the channel from a goroutine so the handler
returns right away. Response gains a `jobID` field and a
`message` string so clients have a consistent handle regardless
of whether the op is queued or running.
Pairs with the OnBackendOpCompleted hook added in the galleryop
commit — together the UI sees the upgrade start, watches
progress via /api/operations, and drops the "upgradeable" flag
the moment the worker finishes.
Assisted-by: Claude:Opus 4.7
2026-04-24 06:50:34 +00:00
|
|
|
|
margin: 0 auto;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
animation: fadeIn var(--duration-normal) var(--ease-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-page__header {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr minmax(240px, 320px);
|
|
|
|
|
|
gap: var(--spacing-lg);
|
|
|
|
|
|
align-items: end;
|
|
|
|
|
|
margin-bottom: var(--spacing-lg);
|
|
|
|
|
|
padding-bottom: var(--spacing-md);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-divider);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-page__header .page-title i {
|
|
|
|
|
|
color: var(--color-accent);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-page__model {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-page__body {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-lg);
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 720px) {
|
|
|
|
|
|
.biometrics-page__header {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Tabs — flat, underlined, inherit page tone */
|
|
|
|
|
|
.biometrics-tabs {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
border-bottom: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
scrollbar-width: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-tabs::-webkit-scrollbar { display: none; }
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-tab {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
font-weight: var(--font-weight-medium);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
border-bottom: 2px solid transparent;
|
|
|
|
|
|
min-height: 44px;
|
|
|
|
|
|
transition: color var(--duration-fast), border-color var(--duration-fast);
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-tab:hover { color: var(--color-text-primary); }
|
|
|
|
|
|
.biometrics-tab.active {
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
border-bottom-color: var(--color-accent);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-tab i { color: var(--color-accent); font-size: 0.9em; }
|
|
|
|
|
|
|
|
|
|
|
|
/* Two-column workflow layout */
|
|
|
|
|
|
.biometrics-twocol {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: minmax(300px, 380px) 1fr;
|
|
|
|
|
|
gap: var(--spacing-lg);
|
|
|
|
|
|
align-items: start;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (max-width: 980px) {
|
|
|
|
|
|
.biometrics-twocol { grid-template-columns: 1fr; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-panel {
|
|
|
|
|
|
background: var(--color-surface-raised);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
padding: var(--spacing-lg);
|
|
|
|
|
|
box-shadow: var(--shadow-subtle), var(--shadow-inset-top);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-panel__title {
|
|
|
|
|
|
font-size: var(--text-lg);
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-panel__title i { color: var(--color-accent); }
|
|
|
|
|
|
.biometrics-panel__note {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
line-height: var(--leading-normal);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-results {
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-empty {
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
border: 1px dashed var(--color-border-default);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
padding: var(--spacing-2xl) var(--spacing-lg);
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
min-height: 300px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-empty > i {
|
|
|
|
|
|
font-size: 2.5rem;
|
|
|
|
|
|
color: var(--color-accent);
|
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-empty h3 {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: var(--text-lg);
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-empty p {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
max-width: 48ch;
|
|
|
|
|
|
line-height: var(--leading-normal);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Media input — file / webcam / record switcher */
|
|
|
|
|
|
.biometrics-mediainput {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__tabs {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
padding: 2px;
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
align-self: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__tab {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
|
min-height: 32px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: var(--font-weight-medium);
|
|
|
|
|
|
transition: background var(--duration-fast), color var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__tab:hover:not(:disabled) { color: var(--color-text-primary); }
|
|
|
|
|
|
.biometrics-mediainput__tab.active {
|
|
|
|
|
|
background: var(--color-surface-raised);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
box-shadow: var(--shadow-subtle);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__tab:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-mediainput__body {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-mediainput__live {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__video {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
aspect-ratio: 4 / 3;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__controls {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__controls .btn { flex: 1; min-height: 40px; }
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-mediainput__meter {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__meter i { color: var(--color-text-muted); }
|
|
|
|
|
|
.biometrics-mediainput__meter.recording {
|
|
|
|
|
|
border-color: var(--color-error-border);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__meter.recording i {
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
animation: biometrics-pulse 1.2s ease-in-out infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
@keyframes biometrics-pulse {
|
|
|
|
|
|
0%, 100% { opacity: 1; }
|
|
|
|
|
|
50% { opacity: 0.35; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-mediainput__error {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-mediainput__notice {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
background: var(--color-warning-light);
|
|
|
|
|
|
border: 1px solid var(--color-warning-border);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
line-height: var(--leading-normal);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__notice > i {
|
|
|
|
|
|
color: var(--color-warning);
|
|
|
|
|
|
margin-top: 3px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__notice strong {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
margin-bottom: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__notice p {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__notice code {
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
padding: 1px 6px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
font-size: 0.95em;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-mediainput__preview {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
padding: var(--spacing-sm);
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__preview img {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-height: 220px;
|
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__preview audio { width: 100%; }
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-mediainput__preview-meta {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__source-pill {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__clear {
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
min-width: 32px;
|
|
|
|
|
|
min-height: 32px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
transition: color var(--duration-fast), background var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-mediainput__clear:hover {
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
background: var(--color-error-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Fieldsets + chip toggles (attribute actions) */
|
|
|
|
|
|
.biometrics-fieldset {
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-fieldset legend {
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.06em;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-chipset {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-chip {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 6px 12px;
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
text-transform: capitalize;
|
|
|
|
|
|
transition: border-color var(--duration-fast), color var(--duration-fast), background var(--duration-fast);
|
|
|
|
|
|
min-height: 32px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-chip input { position: absolute; opacity: 0; pointer-events: none; }
|
|
|
|
|
|
.biometrics-chip:hover { color: var(--color-text-primary); }
|
|
|
|
|
|
.biometrics-chip.active {
|
|
|
|
|
|
border-color: var(--color-accent-border);
|
|
|
|
|
|
background: var(--color-accent-light);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Toggle switch */
|
|
|
|
|
|
.biometrics-switch {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 40px;
|
|
|
|
|
|
height: 22px;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-switch input {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-switch > span {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
background: var(--color-toggle-off);
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
transition: background var(--duration-fast);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-switch > span::after {
|
|
|
|
|
|
content: "";
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: 2px;
|
|
|
|
|
|
top: 2px;
|
|
|
|
|
|
width: 18px;
|
|
|
|
|
|
height: 18px;
|
|
|
|
|
|
border-radius: 50%;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
transition: transform var(--duration-fast);
|
|
|
|
|
|
box-shadow: var(--shadow-subtle);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-switch input:checked + span { background: var(--color-accent); }
|
|
|
|
|
|
.biometrics-switch input:checked + span::after { transform: translateX(18px); }
|
|
|
|
|
|
.biometrics-switch input:focus-visible + span {
|
|
|
|
|
|
outline: 2px solid var(--color-border-focus);
|
|
|
|
|
|
outline-offset: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Split view for analyze (image + summary side) */
|
|
|
|
|
|
.biometrics-split {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: minmax(0, 1.1fr) minmax(280px, 1fr);
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
align-items: start;
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (max-width: 980px) {
|
|
|
|
|
|
.biometrics-split { grid-template-columns: 1fr; }
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-split__media {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-split__aside {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Bounding box overlay */
|
|
|
|
|
|
.biometrics-bbox {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
line-height: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-bbox img {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: auto;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-bbox__box {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
border: 2px solid var(--color-accent);
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.25), 0 0 12px rgba(232, 168, 124, 0.35);
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
transition: border-color var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-bbox__box.tone-default { border-color: var(--color-border-strong); box-shadow: none; }
|
|
|
|
|
|
.biometrics-bbox__box.tone-success { border-color: var(--color-success); }
|
|
|
|
|
|
.biometrics-bbox__box.tone-error { border-color: var(--color-error); }
|
|
|
|
|
|
.biometrics-bbox__box.tone-warning { border-color: var(--color-warning); }
|
|
|
|
|
|
.biometrics-bbox__tag {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
left: -2px;
|
|
|
|
|
|
top: -2px;
|
|
|
|
|
|
transform: translateY(-100%);
|
|
|
|
|
|
background: var(--color-bg-overlay);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-bottom: 0;
|
|
|
|
|
|
border-radius: var(--radius-sm) var(--radius-sm) 0 0;
|
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
line-height: var(--leading-snug);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-bbox__tag strong { font-weight: var(--font-weight-semibold); }
|
|
|
|
|
|
.biometrics-bbox__tag span { color: var(--color-text-secondary); }
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-facepicker {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-facepicker__chip {
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
padding: 4px 12px;
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font: inherit;
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
min-height: 32px;
|
|
|
|
|
|
transition: border-color var(--duration-fast), color var(--duration-fast), background var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-facepicker__chip:hover { color: var(--color-text-primary); }
|
|
|
|
|
|
.biometrics-facepicker__chip.active {
|
|
|
|
|
|
border-color: var(--color-accent-border);
|
|
|
|
|
|
background: var(--color-accent-light);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-facepicker__chip small { margin-left: 4px; color: var(--color-text-muted); }
|
|
|
|
|
|
|
|
|
|
|
|
/* Summary card (dominant attributes) */
|
|
|
|
|
|
.biometrics-summary {
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-summary__head {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
margin-bottom: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-summary__head h3 {
|
|
|
|
|
|
font-size: var(--text-base);
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-summary__head h3 i { color: var(--color-accent); }
|
|
|
|
|
|
.biometrics-summary__head h3 small {
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-weight: var(--font-weight-regular);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-summary__grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: max-content 1fr;
|
|
|
|
|
|
column-gap: var(--spacing-md);
|
|
|
|
|
|
row-gap: 6px;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-summary__grid dt {
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.06em;
|
|
|
|
|
|
align-self: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-summary__grid dd {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
font-weight: var(--font-weight-medium);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Distribution bars */
|
|
|
|
|
|
.biometrics-dist {
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-dist__head {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-dist__head h3 {
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
letter-spacing: -0.005em;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-dist__head i { color: var(--color-accent); }
|
|
|
|
|
|
.biometrics-dist__dominant {
|
|
|
|
|
|
margin-left: auto;
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
text-transform: capitalize;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-dist__rows {
|
|
|
|
|
|
list-style: none;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-dist__row {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: minmax(80px, 110px) 1fr max-content;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-dist__label {
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
text-transform: capitalize;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-dist__bar-wrap {
|
|
|
|
|
|
height: 6px;
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-dist__bar {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: var(--color-text-muted);
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
transition: width var(--duration-normal) var(--ease-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-dist__row.dominant .biometrics-dist__label { color: var(--color-text-primary); }
|
|
|
|
|
|
.biometrics-dist__row.dominant .biometrics-dist__bar { background: var(--color-accent); }
|
|
|
|
|
|
.biometrics-dist__value {
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-dist__row.dominant .biometrics-dist__value { color: var(--color-text-primary); }
|
|
|
|
|
|
|
|
|
|
|
|
/* Pill chips (liveness) */
|
|
|
|
|
|
.biometrics-pill {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 6px;
|
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: var(--font-weight-medium);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-pill small {
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-pill.good {
|
|
|
|
|
|
background: var(--color-success-light);
|
|
|
|
|
|
border-color: var(--color-success-border);
|
|
|
|
|
|
color: var(--color-success);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-pill.bad {
|
|
|
|
|
|
background: var(--color-error-light);
|
|
|
|
|
|
border-color: var(--color-error-border);
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-pill.muted { color: var(--color-text-muted); }
|
|
|
|
|
|
|
|
|
|
|
|
/* Compare view */
|
|
|
|
|
|
.biometrics-compare {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 1fr minmax(280px, 360px) 1fr;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
|
}
|
|
|
|
|
|
@media (max-width: 1080px) {
|
|
|
|
|
|
.biometrics-compare { grid-template-columns: 1fr; }
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-compare__panel {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-compare__label {
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.06em;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-compare__center {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-compare__threshold {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
background: var(--color-surface-raised);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-compare__threshold label {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
font-weight: var(--font-weight-medium);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-compare__threshold code {
|
|
|
|
|
|
color: var(--color-accent);
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-compare__threshold input[type="range"] {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
accent-color: var(--color-accent);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-compare__hint {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-compare__hint code { color: var(--color-text-secondary); }
|
|
|
|
|
|
|
|
|
|
|
|
/* Match gauge */
|
|
|
|
|
|
.biometrics-gauge {
|
|
|
|
|
|
background: var(--color-surface-raised);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
box-shadow: var(--shadow-subtle), var(--shadow-inset-top);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-gauge__head {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-gauge__verdict {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 8px;
|
|
|
|
|
|
font-size: var(--text-lg);
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-gauge.tone-success .biometrics-gauge__verdict { color: var(--color-success); }
|
|
|
|
|
|
.biometrics-gauge.tone-error .biometrics-gauge__verdict { color: var(--color-error); }
|
|
|
|
|
|
.biometrics-gauge__confidence {
|
|
|
|
|
|
text-align: right;
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
|
line-height: var(--leading-tight);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-gauge__confidence strong {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: var(--text-xl);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-gauge__confidence span {
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.06em;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-gauge__track {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
height: 18px;
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-gauge__zone {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
transition: width var(--duration-normal) var(--ease-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-gauge__zone--match {
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
background: var(--color-success-light);
|
|
|
|
|
|
border-right: 1px dashed var(--color-success-border);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-gauge__zone--miss {
|
|
|
|
|
|
background: var(--color-error-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-gauge__threshold {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
width: 2px;
|
|
|
|
|
|
background: var(--color-border-strong);
|
|
|
|
|
|
transform: translateX(-1px);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-gauge__threshold span {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
bottom: 100%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
font-size: 9px;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
|
padding: 1px 4px;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-gauge__marker {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: -4px;
|
|
|
|
|
|
bottom: -4px;
|
|
|
|
|
|
width: 12px;
|
|
|
|
|
|
transform: translateX(-6px);
|
|
|
|
|
|
background: var(--color-text-primary);
|
|
|
|
|
|
border-radius: 2px;
|
|
|
|
|
|
border: 2px solid var(--color-surface-raised);
|
|
|
|
|
|
transition: left var(--duration-normal) var(--ease-default);
|
|
|
|
|
|
box-shadow: var(--shadow-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-gauge__marker span {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 100%;
|
|
|
|
|
|
left: 50%;
|
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
|
font-size: 9px;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
letter-spacing: 0.08em;
|
|
|
|
|
|
padding-top: 4px;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-gauge__footer {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-gauge__footer em {
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.06em;
|
|
|
|
|
|
font-style: normal;
|
|
|
|
|
|
margin-right: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-gauge__footer code {
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Waveform */
|
|
|
|
|
|
.biometrics-waveform {
|
|
|
|
|
|
--biometrics-wave: var(--color-accent);
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-waveform--error {
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-waveform__segment {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
background: rgba(232, 168, 124, 0.16);
|
|
|
|
|
|
border-left: 1px dashed var(--color-accent-border);
|
|
|
|
|
|
border-right: 1px dashed var(--color-accent-border);
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-waveform__segment.tone-info { background: var(--color-info-light); border-color: var(--color-info-border); }
|
|
|
|
|
|
.biometrics-waveform__segment.tone-success { background: var(--color-success-light); border-color: var(--color-success-border); }
|
|
|
|
|
|
.biometrics-waveform__segment.tone-warning { background: var(--color-warning-light); border-color: var(--color-warning-border); }
|
|
|
|
|
|
.biometrics-waveform__segment.tone-accent { background: var(--color-accent-light); border-color: var(--color-accent-border); }
|
|
|
|
|
|
.biometrics-waveform__seglabel {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 4px;
|
|
|
|
|
|
left: 4px;
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
background: var(--color-bg-overlay);
|
|
|
|
|
|
padding: 1px 6px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
max-width: calc(100% - 8px);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-waveform__duration {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 8px;
|
|
|
|
|
|
bottom: 6px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
|
background: var(--color-bg-overlay);
|
|
|
|
|
|
padding: 1px 6px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-waveform__loading {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: add LocalVQE backend and audio transformations UI (#9640)
feat(audio-transform): add LocalVQE backend, bidi gRPC RPC, Studio UI
Introduce a generic "audio transform" capability for any audio-in / audio-out
operation (echo cancellation, noise suppression, dereverberation, voice
conversion, etc.) and ship LocalVQE as the first backend implementation.
Backend protocol:
- Two new gRPC RPCs in backend.proto: unary AudioTransform for batch and
bidirectional AudioTransformStream for low-latency frame-by-frame use.
This is the first bidi stream in the proto; per-frame unary at LocalVQE's
16 ms hop would be RTT-bound. Wire it through pkg/grpc/{client,server,
embed,interface,base} with paired-channel ergonomics.
LocalVQE backend (backend/go/localvqe/):
- Go-Purego wrapper around upstream liblocalvqe.so. CMake builds the upstream
shared lib + its libggml-cpu-*.so runtime variants directly — no MODULE
wrapper needed because LocalVQE handles CPU feature selection internally
via GGML_BACKEND_DL.
- Sets GGML_NTHREADS from opts.Threads (or runtime.NumCPU()-1) — without it
LocalVQE runs single-threaded at ~1× realtime instead of the documented
~9.6×.
- Reference-length policy: zero-pad short refs, truncate long ones (the
trailing portion can't have leaked into a mic that wasn't recording).
- Ginkgo test suite (9 always-on specs + 2 model-gated).
HTTP layer:
- POST /audio/transformations (alias /audio/transform): multipart batch
endpoint, accepts audio + optional reference + params[*]=v form fields.
Persists inputs alongside the output in GeneratedContentDir/audio so the
React UI history can replay past (audio, reference, output) triples.
- GET /audio/transformations/stream: WebSocket bidi, 16 ms PCM frames
(interleaved stereo mic+ref in, mono out). JSON session.update envelope
for config; constants hoisted in core/schema/audio_transform.go.
- ffmpeg-based input normalisation to 16 kHz mono s16 WAV via the existing
utils.AudioToWav (with passthrough fast-path), so the user can upload any
format / rate without seeing the model's strict 16 kHz constraint.
- BackendTraceAudioTransform integration so /api/backend-traces and the
Traces UI light up with audio_snippet base64 and timing.
- Routes registered under routes/localai.go (LocalAI extension; OpenAI has
no /audio/transformations endpoint), traced via TraceMiddleware.
Auth + capability + importer:
- FLAG_AUDIO_TRANSFORM (model_config.go), FeatureAudioTransform (default-on,
in APIFeatures), three RouteFeatureRegistry rows.
- localvqe added to knownPrefOnlyBackends with modality "audio-transform".
- Gallery entry localvqe-v1-1.3m (sha256-pinned, hosted on
huggingface.co/LocalAI-io/LocalVQE).
React UI:
- New /app/transform page surfaced via a dedicated "Enhance" sidebar
section (sibling of Tools / Biometrics) — the page is enhancement, not
generation, so it lives outside Studio. Two AudioInput components
(Upload + Record tabs, drag-drop, mic capture).
- Echo-test button: records mic while playing the loaded reference through
the speakers — the mic naturally picks up speaker bleed, giving a real
(mic, ref) pair for AEC testing without leaving the UI.
- Reusable WaveformPlayer (canvas peaks + click-to-seek + audio controls)
and useAudioPeaks hook (shared module-scoped AudioContext to avoid
hitting browser context limits with three players on one page); migrated
TTS, Sound, Traces audio blocks to use it.
- Past runs saved in localStorage via useMediaHistory('audio-transform') —
the history entry stores all three URLs so clicking re-renders the full
triple, not just the output.
Build + e2e:
- 11 matrix entries removed from .github/workflows/backend.yml (CUDA, ROCm,
SYCL, Metal, L4T): upstream supports only CPU + Vulkan, so we ship those
two and let GPU-class hardware route through Vulkan in the gallery
capabilities map.
- tests-localvqe-grpc-transform job in test-extra.yml (gated on
detect-changes.outputs.localvqe).
- New audio_transform capability + 4 specs in tests/e2e-backends.
- Playwright spec suite in core/http/react-ui/e2e/audio-transform.spec.js
(8 specs covering tabs, file upload, multipart shape, history, errors).
Docs:
- New docs/content/features/audio-transform.md covering the (audio,
reference) mental model, batch + WebSocket wire formats, LocalVQE param
keys, and a YAML config example. Cross-links from text-to-audio and
audio-to-text feature pages.
Assisted-by: Claude:claude-opus-4-7 [Bash Read Edit Write Agent TaskCreate]
Signed-off-by: Richard Palethorpe <[email protected]>
2026-05-04 20:07:11 +00:00
|
|
|
|
/* Reusable waveform-and-playback component (audio transform / TTS / sound / traces) */
|
|
|
|
|
|
.audio-waveform-player {
|
|
|
|
|
|
--audio-wave: var(--color-primary);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-waveform-player--dimmed .audio-waveform-player__canvas-wrap {
|
|
|
|
|
|
opacity: 0.7;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-waveform-player__label {
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-waveform-player__canvas-wrap {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-waveform-player__error {
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-waveform-player__segment {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
background: rgba(136, 192, 208, 0.16);
|
|
|
|
|
|
border-left: 1px dashed var(--color-primary);
|
|
|
|
|
|
border-right: 1px dashed var(--color-primary);
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-waveform-player__seglabel {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 4px;
|
|
|
|
|
|
left: 4px;
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
background: var(--color-bg-overlay);
|
|
|
|
|
|
padding: 1px 6px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
max-width: calc(100% - 8px);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-waveform-player__duration {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
right: 8px;
|
|
|
|
|
|
bottom: 6px;
|
|
|
|
|
|
font-size: 11px;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
|
background: var(--color-bg-overlay);
|
|
|
|
|
|
padding: 1px 6px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-waveform-player__loading {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
inset: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-waveform-player__playhead {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
bottom: 0;
|
|
|
|
|
|
width: 1.5px;
|
|
|
|
|
|
background: var(--color-primary);
|
|
|
|
|
|
opacity: 0.85;
|
|
|
|
|
|
pointer-events: none;
|
|
|
|
|
|
transform: translateX(-0.75px);
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-waveform-player__player {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-waveform-player__download {
|
|
|
|
|
|
align-self: flex-end;
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Audio Transform Studio tab */
|
|
|
|
|
|
.audio-transform-stack {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-transform-drop {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
border: 1px dashed var(--color-border);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
cursor: default;
|
|
|
|
|
|
transition: border-color var(--duration-normal, 180ms) var(--ease-default, ease);
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-transform-drop--hover {
|
|
|
|
|
|
border-color: var(--color-primary);
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-transform-drop__file {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
word-break: break-all;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-transform-drop__pick {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
color: var(--color-primary);
|
|
|
|
|
|
text-decoration: underline;
|
|
|
|
|
|
margin-left: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-transform-input {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-transform-input__tabs {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
gap: 2px;
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
padding: 2px;
|
|
|
|
|
|
align-self: flex-start;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-transform-input__tab {
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
padding: 4px 10px;
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-transform-input__tab.active {
|
|
|
|
|
|
background: var(--color-primary-light);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-transform-rec {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-transform-rec__notice {
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-transform-rec__notice--error {
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-transform-rec__pending {
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-style: italic;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-transform-echo {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: stretch;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-transform-echo__row {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-transform-echo__notice {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
background: var(--color-info-light);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
border-left: 3px solid var(--color-info);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-transform-echo__notice > i {
|
|
|
|
|
|
color: var(--color-info);
|
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.audio-transform-echo__elapsed {
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat: add biometrics UI (#9524)
* feat(react-ui): add Face & Voice Recognition pages
Expose the face and voice biometrics endpoints
(/v1/face/*, /v1/voice/*) through the React UI. Each page has four
tabs driving the six endpoints per modality: Analyze (demographics
with bounding boxes / waveform segments), Compare (verify with a
match gauge and live threshold slider), Enrollment (register /
identify / forget with a top-K matches view), Embedding (raw
vector inspector with sparkline + copy).
MediaInput supports file upload plus live capture: webcam
snap-to-canvas for face, MediaRecorder -> AudioContext ->
16-bit PCM mono WAV transcode for voice (libsndfile on the
backend only handles WAV/FLAC/OGG natively).
Sidebar gets a new Biometrics section feature-gated on
face_recognition / voice_recognition; routes are wrapped in
<RequireFeature>. No new dependencies -- Font Awesome icons
picked from the Free set.
Assisted-by: Claude:Opus 4.7
* fix(localai): accept data URI prefixes with codec/charset params
Browser MediaRecorder produces data URIs like
data:audio/webm;codecs=opus;base64,...
so the pre-';base64,' section can carry multiple parameter
segments. The `^data:([^;]+);base64,` regex in pkg/utils/base64.go
and core/http/endpoints/localai/audio.go only matched exactly one
segment, so recordings straight from the React UI's live-capture
tab failed the strip and then tripped the base64 decoder on the
leading 'data:' literal, surfacing as
"invalid audio base64: illegal base64 data at input byte 4"
Widened both regexes to `^data:[^,]+?;base64,` so any number of
';param=value' segments between the mime type and ';base64,' are
tolerated. Added a regression test covering the MediaRecorder
shape.
Assisted-by: Claude:Opus 4.7
* fix(insightface): scope pack ONNX loading to known manifests
LocalAI's gallery extracts buffalo_* zips flat into the models
directory, which inevitably mixes with ONNX files from other
backends (opencv face engine, MiniFASNet antispoof, WeSpeaker
voice embedding) and older buffalo pack installs. Feeding those
foreign files into insightface's model_zoo.get_model() blows up
inside the router -- it assumes a 4-D NCHW input and indexes
`input_shape[2]` on tensors that aren't shaped like a face model,
raising IndexError mid-load and leaving the backend unusable.
The router's dispatch isn't amenable to per-file try/except alone
(first-file-wins picks det_10g.onnx from buffalo_l even when the
user asked for buffalo_sc -- alphabetical order happens to favour
the wrong pack). Instead, ship an explicit manifest of the
upstream v0.7 pack contents and scope the glob to that when the
requested pack is known. The manifest is small and stable; future
packs can be added alongside or fall through to the tolerance
loop, which also swallows any remaining IndexError / ValueError
from foreign files with a clear `[insightface] skipped` stderr
line for diagnostics.
Assisted-by: Claude:Opus 4.7
* fix(speaker-recognition): extract FBank features for rank-3 ONNX encoders
Pre-exported speaker-encoder ONNX graphs come in two shapes:
rank-2 [batch, samples] -- some 3D-Speaker exports,
take raw waveform directly.
rank-3 [batch, frames, n_mels] -- WeSpeaker and most Kaldi-
lineage encoders, expect
pre-computed Kaldi FBank.
OnnxDirectEngine unconditionally fed `audio.reshape(1, -1)` --
correct for rank-2, IndexError-on-input_shape[3] on rank-3, which
surfaced to the UI as
"Invalid rank for input: feats Got: 2 Expected: 3"
Detect the input rank at session init and run Kaldi FBank
(80-dim, 25ms/10ms frames, dither=0.0, per-utterance CMN) before
the forward pass when rank>=3. All knobs are configurable via
backend options for encoders that deviate from defaults.
torchaudio.compliance.kaldi is already in the backend's
requirements (SpeechBrain pulls torchaudio in), so no new
dependency.
Assisted-by: Claude:Opus 4.7
* fix(biometrics): isolate face and voice vector stores
Face (ArcFace, 512-D) and voice (ECAPA-TDNN 192-D / WeSpeaker
256-D) biometric embeddings were colliding inside a single
in-memory local-store instance. Enrolling one after the other
failed with
"Try to add key with length N when existing length is M"
because local-store correctly refuses to mix dimensions in one
keyspace.
The registries were constructed with `storeName=""`, which in
StoreBackend() is just a WithModel() call. But ModelLoader's
cache is keyed on `modelID`, not `model` -- so both registries
collapsed to the same `modelID=""` slot and reused the same
backend process despite looking isolated on paper.
Three complementary fixes:
1. application.go -- give each registry a distinct default
namespace ("localai-face-biometrics" /
"localai-voice-biometrics"). The comment claimed
isolation, now it's actually enforced.
2. stores.go -- pass the storeName as both WithModelID and
WithModel so the ModelLoader cache key separates
namespaces and the loader spawns distinct processes.
3. local-store/store.go -- drop the Load() `opts.Model != ""`
guard. It was there to prevent generic model-loading loops
from picking up local-store by accident, but that auto-load
path is being retired; the guard now just blocks legitimate
namespace isolation. opts.Model is treated as a tag; the
per-tuple process isolation upstream handles discrimination.
Assisted-by: Claude:Opus 4.7
* fix(gallery): stale-file cleanup and upgrade-tmp directory safety
Two related robustness fixes for backend install/upgrade:
pkg/downloader/uri.go
OCI downloads passed through
if filepath.Ext(filePath) != "" ...
filePath = filepath.Dir(filePath)
which was intended to redirect file-shaped download targets
into their parent directory for OCI extraction. The heuristic
misfires on directory-shaped paths with a dot-suffix --
gallery.UpgradeBackend uses
tmpPath = "<backendsPath>/<name>.upgrade-tmp"
and Go's filepath.Ext treats ".upgrade-tmp" as an extension.
The rewrite landed the extraction at "<backendsPath>/", which
then **overwrote the real install** (backends/<name>/) with a
flat-layout file and left a stray run.sh at the top level. The
tmp dir itself stayed empty, so the validation step that
checked "<tmpPath>/run.sh" predictably failed with
"upgrade validation failed: run.sh not found in new backend"
Every manual upgrade silently corrupted the backends tree this
way. Guard the rewrite behind "target isn't already an existing
directory" -- InstallBackend / UpgradeBackend both pre-create
the target as a directory, so they get the correct behaviour;
existing file-path callers with a genuine dot-extension still
get the parent redirect.
core/gallery/backends.go
InstallBackend's MkdirAll returned ENOTDIR when something at
the target path was already a file (legacy dev builds dropped
golang backend binaries directly at `<backendsPath>/<name>`
instead of nesting them under their own subdir). That
permanently blocked reinstall and upgrade for anyone carrying
that state, since every retry hit the same error. Detect a
pre-existing non-directory, warn, and remove it before the
MkdirAll so the fresh install can write the correct nested
layout with metadata.json + run.sh.
Assisted-by: Claude:Opus 4.7
* fix(galleryop): refresh upgrade cache after backend ops
UpgradeChecker caches the last upgrade-check result and only
refreshes on the 6-hour tick or after an auto-upgrade cycle.
Manual upgrades (POST /api/backends/upgrade/:name) go through
the async galleryop worker, which completes the upgrade
correctly but never tells UpgradeChecker to re-check -- so
/api/backends/upgrades continued to list a just-upgraded backend
as upgradeable, indistinguishable from a failed upgrade, for up
to six hours.
Add an optional `OnBackendOpCompleted func()` hook on
GalleryService that fires after every successful install /
upgrade / delete on the backend channel (async, so a slow
callback doesn't stall the queue). startup.go wires it to
UpgradeChecker.TriggerCheck after both services exist. Result:
the upgrade banner clears within milliseconds of the worker
finishing.
Assisted-by: Claude:Opus 4.7
* build: prepend GOPATH/bin to PATH for protogen-go
install-go-tools runs `go install` for protoc-gen-go and
protoc-gen-go-grpc, which writes them into `go env GOPATH`/bin.
That directory isn't on every dev's PATH, and protoc resolves
its code-gen plugins via PATH, so the immediately-following
protoc invocation fails with
"protoc-gen-go: program not found"
which in turn blocks `make build` and any
`make backends/%` target that depends on build.
Prepend `go env GOPATH`/bin to PATH for the protoc invocation
so the freshly-installed plugins are found without requiring a
shell-profile change.
Assisted-by: Claude:Opus 4.7
* refactor(ui-api): non-blocking backend upgrade handler with opcache
POST /api/backends/upgrade/:name used to send the ManagementOp
directly onto the unbuffered BackendGalleryChannel, which blocked
the HTTP request whenever the galleryop worker was busy with a
prior operation. The op also didn't show up in /api/operations,
so the Backends UI couldn't reflect upgrade progress on the
affected row.
Register the op in opcache immediately, wrap it in a cancellable
context, store the cancellation function on the GalleryService,
and push onto the channel from a goroutine so the handler
returns right away. Response gains a `jobID` field and a
`message` string so clients have a consistent handle regardless
of whether the op is queued or running.
Pairs with the OnBackendOpCompleted hook added in the galleryop
commit — together the UI sees the upgrade start, watches
progress via /api/operations, and drops the "upgradeable" flag
the moment the worker finishes.
Assisted-by: Claude:Opus 4.7
2026-04-24 06:50:34 +00:00
|
|
|
|
/* Enrollment layout (register + identify + list) */
|
|
|
|
|
|
.biometrics-enrollgrid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: minmax(300px, 1fr) minmax(300px, 1fr);
|
|
|
|
|
|
grid-template-areas:
|
|
|
|
|
|
"register identify"
|
|
|
|
|
|
"list list";
|
|
|
|
|
|
gap: var(--spacing-lg);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enrollgrid__register { grid-area: register; }
|
|
|
|
|
|
.biometrics-enrollgrid__identify { grid-area: identify; }
|
|
|
|
|
|
.biometrics-enrollgrid__list { grid-area: list; min-width: 0; }
|
|
|
|
|
|
@media (max-width: 980px) {
|
|
|
|
|
|
.biometrics-enrollgrid {
|
|
|
|
|
|
grid-template-columns: 1fr;
|
|
|
|
|
|
grid-template-areas:
|
|
|
|
|
|
"register"
|
|
|
|
|
|
"identify"
|
|
|
|
|
|
"list";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enrollgrid__register form,
|
|
|
|
|
|
.biometrics-enrollgrid__identify form {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enrollgrid__err {
|
|
|
|
|
|
margin-top: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-enroll__head {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
margin-bottom: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enroll__count {
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: var(--font-weight-medium);
|
|
|
|
|
|
padding: 2px 8px;
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
margin-left: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-enroll__grid {
|
|
|
|
|
|
list-style: none;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enroll__card {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
background: var(--color-surface-raised);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
transition: border-color var(--duration-fast), transform var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enroll__card:hover {
|
|
|
|
|
|
border-color: var(--color-border-default);
|
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enroll__card.highlight {
|
|
|
|
|
|
border-color: var(--color-accent-border);
|
|
|
|
|
|
box-shadow: 0 0 0 1px var(--color-accent-border);
|
|
|
|
|
|
animation: biometrics-highlight 1.4s ease-out;
|
|
|
|
|
|
}
|
|
|
|
|
|
@keyframes biometrics-highlight {
|
|
|
|
|
|
0% { box-shadow: 0 0 0 4px var(--color-accent-light); }
|
|
|
|
|
|
100% { box-shadow: 0 0 0 1px var(--color-accent-border); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.biometrics-enroll__media {
|
|
|
|
|
|
aspect-ratio: 1 / 1;
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enroll__media img {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enroll__media audio {
|
|
|
|
|
|
width: 90%;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enroll__initials {
|
|
|
|
|
|
font-size: 2rem;
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
letter-spacing: 0.04em;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enroll__body { display: flex; flex-direction: column; gap: 4px; }
|
|
|
|
|
|
.biometrics-enroll__name {
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enroll__labels {
|
|
|
|
|
|
list-style: none;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enroll__labels li {
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enroll__labels li span {
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
margin-right: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enroll__meta {
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enroll__delete {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 8px;
|
|
|
|
|
|
right: 8px;
|
|
|
|
|
|
background: var(--color-bg-overlay);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
width: 28px;
|
|
|
|
|
|
height: 28px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
opacity: 0;
|
|
|
|
|
|
transition: opacity var(--duration-fast), color var(--duration-fast), background var(--duration-fast);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enroll__card:hover .biometrics-enroll__delete,
|
|
|
|
|
|
.biometrics-enroll__card:focus-within .biometrics-enroll__delete { opacity: 1; }
|
|
|
|
|
|
.biometrics-enroll__delete:hover {
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
background: var(--color-error-light);
|
|
|
|
|
|
border-color: var(--color-error-border);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enroll__empty {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: var(--spacing-xl);
|
|
|
|
|
|
border: 1px dashed var(--color-border-default);
|
|
|
|
|
|
border-radius: var(--radius-lg);
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enroll__empty > i {
|
|
|
|
|
|
font-size: 2rem;
|
|
|
|
|
|
color: var(--color-accent);
|
|
|
|
|
|
opacity: 0.6;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-enroll__empty p {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
max-width: 44ch;
|
|
|
|
|
|
line-height: var(--leading-normal);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Matches list (identify results) */
|
|
|
|
|
|
.biometrics-matches {
|
|
|
|
|
|
list-style: none;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-matches__empty {
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
border: 1px dashed var(--color-border-default);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-matches__row {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: 32px 56px 1fr;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
padding: var(--spacing-sm);
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-matches__row.match { border-color: var(--color-success-border); }
|
|
|
|
|
|
.biometrics-matches__rank {
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-matches__avatar {
|
|
|
|
|
|
width: 56px;
|
|
|
|
|
|
height: 56px;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-matches__avatar img { width: 100%; height: 100%; object-fit: cover; }
|
|
|
|
|
|
.biometrics-matches__body { min-width: 0; display: flex; flex-direction: column; gap: 4px; }
|
|
|
|
|
|
.biometrics-matches__name {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-matches__name strong {
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-matches__badge {
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.06em;
|
|
|
|
|
|
padding: 2px 6px;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-matches__badge.match {
|
|
|
|
|
|
background: var(--color-success-light);
|
|
|
|
|
|
color: var(--color-success);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-matches__meter {
|
|
|
|
|
|
height: 4px;
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
border-radius: var(--radius-full);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-matches__fill {
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
background: var(--color-accent);
|
|
|
|
|
|
transition: width var(--duration-normal) var(--ease-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-matches__row.match .biometrics-matches__fill { background: var(--color-success); }
|
|
|
|
|
|
.biometrics-matches__meta {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-matches__meta code {
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
font-variant-numeric: tabular-nums;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-matches__preview { width: 100%; }
|
|
|
|
|
|
|
|
|
|
|
|
/* Embedding inspector */
|
|
|
|
|
|
.biometrics-embed {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-embed__head {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: flex-start;
|
|
|
|
|
|
justify-content: space-between;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-embed__title {
|
|
|
|
|
|
font-size: var(--text-base);
|
|
|
|
|
|
font-weight: var(--font-weight-semibold);
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-embed__meta {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
gap: var(--spacing-md);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
margin-top: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-embed__meta strong { color: var(--color-text-primary); font-variant-numeric: tabular-nums; font-weight: var(--font-weight-semibold); }
|
|
|
|
|
|
.biometrics-embed__meta code { color: var(--color-text-secondary); }
|
|
|
|
|
|
|
|
|
|
|
|
/* Response details pane */
|
|
|
|
|
|
.biometrics-response {
|
|
|
|
|
|
background: var(--color-bg-secondary);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-response summary {
|
|
|
|
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
list-style: none;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
min-height: 40px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.biometrics-response summary::-webkit-details-marker { display: none; }
|
|
|
|
|
|
.biometrics-response summary i { transition: transform var(--duration-fast); }
|
|
|
|
|
|
.biometrics-response[open] summary i { transform: rotate(90deg); }
|
|
|
|
|
|
.biometrics-response pre {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: var(--spacing-md);
|
|
|
|
|
|
background: var(--color-surface-sunken);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
overflow-x: auto;
|
|
|
|
|
|
max-height: 360px;
|
|
|
|
|
|
line-height: var(--leading-snug);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.form-label__hint {
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-weight: var(--font-weight-regular);
|
|
|
|
|
|
margin-left: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
feat(react-ui): polish Manage page with kebab menus and gallery rows
Bring the System / Manage page up to the visual standard of the Install
gallery so installed models and backends stop reading like a debug dump.
- Unified ResourceRow anatomy (icon, name+description, badges, status,
expandable detail) shared across both tabs.
- Gallery enrichment cross-references installed names against the gallery
list endpoints to surface icons, descriptions, license, tags, and links
with a graceful "no description" fallback for custom imports.
- Header summary with four StatCards (Models / Backends / Running /
Updates) — clickable to switch tab + pre-set filter.
- Backends meta + development entries hidden by default; "Show meta &
development" paired toggle in the FilterBar with hidden-count hint.
- Kebab (three-dot) ActionMenu replaces the inline button cluster on
every row; restrained until hover, keyboard-navigable, danger items
separated by a divider.
- Backend "Version" cell now falls back to short digest, OCI tag, or
ocifile basename when no semver is set, instead of showing "—" for
every OCI install. Detail panel exposes full Source URI + Digest.
- Drop redundant column headers ("Actions", "On") — kebabs and toggles
carry their own affordance; screen readers still get a label.
- Inline System / User / Meta / Dev badges next to the backend name so
the dedicated Type column doesn't reserve space for "USER" repeated.
- Tightened the spacing between the System Resources card and the
StatCards so they no longer crowd the RAM bar.
Extracted StatCard and GalleryLoader from Nodes.jsx and Models.jsx into
shared components so the visual language is one source of truth.
Signed-off-by: Ettore Di Giacinto <[email protected]>
Assisted-by: Claude Code:claude-opus-4-7 [Read] [Edit] [Write] [Bash]
2026-04-26 20:33:49 +00:00
|
|
|
|
/* ResourceRow — unified expandable row anatomy used by the Manage page so
|
|
|
|
|
|
installed models and backends share the same visual grammar as the Install
|
|
|
|
|
|
gallery (icon, name, description, badges, expandable detail panel). */
|
|
|
|
|
|
.resource-row { transition: background var(--duration-fast) var(--ease-default); }
|
|
|
|
|
|
.resource-row.is-dimmed { opacity: 0.55; transition: opacity 0.2s; }
|
|
|
|
|
|
.resource-row.is-expanded { background: var(--color-bg-tertiary); }
|
|
|
|
|
|
|
|
|
|
|
|
.resource-row__chevron-cell { width: 30px; }
|
|
|
|
|
|
.resource-row__icon-cell { width: 64px; }
|
|
|
|
|
|
|
|
|
|
|
|
.resource-row__icon {
|
|
|
|
|
|
width: 48px;
|
|
|
|
|
|
height: 48px;
|
|
|
|
|
|
border-radius: var(--radius-md);
|
|
|
|
|
|
border: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
background: var(--color-bg-primary);
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-row__icon > img {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
object-fit: cover;
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-row__icon > i {
|
|
|
|
|
|
font-size: 1.25rem;
|
|
|
|
|
|
color: var(--color-accent);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.resource-row__detail-row > .resource-row__detail-cell {
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
background: var(--color-bg-primary);
|
|
|
|
|
|
border-top: 1px solid var(--color-border-subtle);
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-row__detail {
|
|
|
|
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-row__detail h4 {
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
letter-spacing: 0.06em;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
margin: 0 0 var(--spacing-sm) 0;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-row__detail h4 i { color: var(--color-accent); }
|
|
|
|
|
|
.resource-row__detail-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: max-content 1fr;
|
|
|
|
|
|
gap: 6px var(--spacing-md);
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-row__detail-grid dt {
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
letter-spacing: 0.06em;
|
|
|
|
|
|
align-self: start;
|
|
|
|
|
|
padding-top: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-row__detail-grid dd {
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
word-break: break-word;
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-row__detail-md {
|
|
|
|
|
|
color: var(--color-text-secondary);
|
|
|
|
|
|
line-height: 1.6;
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-row__detail-md p:first-child { margin-top: 0; }
|
|
|
|
|
|
.resource-row__detail-md p:last-child { margin-bottom: 0; }
|
|
|
|
|
|
|
|
|
|
|
|
/* Description line directly under the row name — Install gallery already
|
|
|
|
|
|
does this; Manage rows used to show only the bare name. */
|
|
|
|
|
|
.resource-row__desc {
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
max-width: 42ch;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
margin-top: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ResourceActions — split lifecycle vs destructive with a thin divider so
|
|
|
|
|
|
the trash icon doesn't sit at the same eye-weight as the routine buttons.
|
|
|
|
|
|
Used now only for the rare row whose actions can't collapse into the
|
|
|
|
|
|
kebab menu (e.g. a "Protected" badge on system backends). */
|
|
|
|
|
|
.resource-actions {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-actions__group {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-actions__divider {
|
|
|
|
|
|
width: 1px;
|
|
|
|
|
|
height: 20px;
|
|
|
|
|
|
background: var(--color-border-subtle);
|
|
|
|
|
|
margin: 0 var(--spacing-xs);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* ActionMenu — kebab trigger + popover menu. Borrows claudemaster's
|
|
|
|
|
|
restrained pattern: button reads as a quiet ellipsis at rest, lights up
|
|
|
|
|
|
on row hover, and the menu items hold typography-first weight (icon +
|
|
|
|
|
|
label, no fills until hover). The trigger stays at low opacity until the
|
|
|
|
|
|
user reaches for the row, so dense tables don't read as control panels. */
|
|
|
|
|
|
.action-menu__trigger {
|
|
|
|
|
|
display: inline-flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
width: 28px;
|
|
|
|
|
|
height: 28px;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
border: 1px solid transparent;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
opacity: 0.45;
|
|
|
|
|
|
transition:
|
|
|
|
|
|
opacity var(--duration-fast) var(--ease-default),
|
|
|
|
|
|
color var(--duration-fast) var(--ease-default),
|
|
|
|
|
|
background var(--duration-fast) var(--ease-default),
|
|
|
|
|
|
border-color var(--duration-fast) var(--ease-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
.resource-row:hover .action-menu__trigger,
|
|
|
|
|
|
.action-menu__trigger:focus-visible,
|
|
|
|
|
|
.action-menu__trigger.is-open {
|
|
|
|
|
|
opacity: 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-menu__trigger:hover,
|
|
|
|
|
|
.action-menu__trigger.is-open {
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
border-color: var(--color-border-subtle);
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-menu__trigger:focus-visible {
|
|
|
|
|
|
outline: 2px solid var(--color-border-focus);
|
|
|
|
|
|
outline-offset: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-menu__trigger--compact {
|
|
|
|
|
|
width: 24px;
|
|
|
|
|
|
height: 24px;
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.action-menu {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
min-width: 200px;
|
|
|
|
|
|
padding: 4px;
|
|
|
|
|
|
outline: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-menu__item {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: 8px 10px;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
border-radius: var(--radius-sm);
|
|
|
|
|
|
background: transparent;
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
font-family: inherit;
|
|
|
|
|
|
font-size: var(--text-sm);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
text-align: left;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
transition: background var(--duration-fast) var(--ease-default),
|
|
|
|
|
|
color var(--duration-fast) var(--ease-default);
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-menu__item.is-active,
|
|
|
|
|
|
.action-menu__item:hover:not(:disabled) {
|
|
|
|
|
|
background: var(--color-bg-tertiary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-menu__item:disabled {
|
|
|
|
|
|
cursor: not-allowed;
|
|
|
|
|
|
opacity: 0.45;
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-menu__icon {
|
|
|
|
|
|
width: 14px;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-menu__item.is-active .action-menu__icon,
|
|
|
|
|
|
.action-menu__item:hover:not(:disabled) .action-menu__icon {
|
|
|
|
|
|
color: var(--color-text-primary);
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-menu__label {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
text-overflow: ellipsis;
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-menu__shortcut {
|
|
|
|
|
|
font-family: var(--font-mono);
|
|
|
|
|
|
font-size: 10px;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
letter-spacing: 0.04em;
|
|
|
|
|
|
flex-shrink: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-menu__item.is-danger { color: var(--color-error); }
|
|
|
|
|
|
.action-menu__item.is-danger .action-menu__icon { color: var(--color-error); }
|
|
|
|
|
|
.action-menu__item.is-danger:hover:not(:disabled),
|
|
|
|
|
|
.action-menu__item.is-danger.is-active {
|
|
|
|
|
|
background: var(--color-error-light);
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-menu__item.is-danger:hover:not(:disabled) .action-menu__icon,
|
|
|
|
|
|
.action-menu__item.is-danger.is-active .action-menu__icon {
|
|
|
|
|
|
color: var(--color-error);
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-menu__divider {
|
|
|
|
|
|
height: 1px;
|
|
|
|
|
|
background: var(--color-border-subtle);
|
|
|
|
|
|
margin: 4px 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-menu__badge {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: var(--spacing-sm);
|
|
|
|
|
|
padding: 8px 10px;
|
|
|
|
|
|
font-size: var(--text-xs);
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
letter-spacing: 0.06em;
|
|
|
|
|
|
text-transform: uppercase;
|
|
|
|
|
|
color: var(--color-text-muted);
|
|
|
|
|
|
}
|
|
|
|
|
|
.action-menu__badge > i { color: var(--color-text-muted); font-size: var(--text-xs); width: 14px; text-align: center; }
|
|
|
|
|
|
|
|
|
|
|
|
/* Pulse modifier — subtle ring on the Pin button when a model is pinned, so
|
|
|
|
|
|
pinning is visible at a glance without a second icon next to the name. */
|
|
|
|
|
|
.btn--pulse {
|
|
|
|
|
|
animation: btnPulseWarning 1.6s ease-in-out infinite;
|
|
|
|
|
|
}
|
|
|
|
|
|
@keyframes btnPulseWarning {
|
|
|
|
|
|
0%, 100% { box-shadow: 0 0 0 0 rgba(235, 203, 139, 0); }
|
|
|
|
|
|
50% { box-shadow: 0 0 0 4px rgba(235, 203, 139, 0.18); }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* StatCard clickable variant — the Manage summary cards double as shortcuts
|
|
|
|
|
|
to the relevant tab + filter, so the card needs a real button affordance. */
|
|
|
|
|
|
.stat-card[data-clickable="true"] {
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
user-select: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-card[data-clickable="true"]:hover {
|
|
|
|
|
|
border-color: var(--stat-accent, var(--color-border));
|
|
|
|
|
|
transform: translateY(-1px);
|
|
|
|
|
|
}
|
|
|
|
|
|
.stat-card[data-clickable="true"]:focus-visible {
|
|
|
|
|
|
outline: 2px solid var(--color-border-focus);
|
|
|
|
|
|
outline-offset: 2px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Manage summary marker — same .stat-grid layout. Top margin separates the
|
|
|
|
|
|
cards from the System Resources card above (otherwise they sit too close
|
|
|
|
|
|
to the RAM bar) and bottom margin tightens the gap to the tabs below. */
|
|
|
|
|
|
.manage-summary {
|
|
|
|
|
|
margin-top: var(--spacing-xl);
|
|
|
|
|
|
margin-bottom: var(--spacing-lg);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* Screen-reader-only label, used for table headers whose visual cell needs
|
|
|
|
|
|
no label (kebab-only Actions column, toggle-only Enabled column). The
|
|
|
|
|
|
header still announces correctly to assistive tech without making the
|
|
|
|
|
|
table feel like it's labelling sparse columns twice. */
|
|
|
|
|
|
.visually-hidden {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
width: 1px;
|
|
|
|
|
|
height: 1px;
|
|
|
|
|
|
padding: 0;
|
|
|
|
|
|
margin: -1px;
|
|
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
clip: rect(0, 0, 0, 0);
|
|
|
|
|
|
white-space: nowrap;
|
|
|
|
|
|
border: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-19 20:40:51 +00:00
|
|
|
|
/* Reduced motion accessibility */
|
|
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
|
|
|
|
*, *::before, *::after {
|
|
|
|
|
|
animation-duration: 0.01ms !important;
|
|
|
|
|
|
animation-iteration-count: 1 !important;
|
|
|
|
|
|
transition-duration: 0.01ms !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|