LocalAI/core/http/react-ui/src/App.css

7649 lines
180 KiB
CSS
Raw Normal View History

/* 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);
}
.sidebar-is-collapsed .main-content {
margin-left: var(--sidebar-width-collapsed);
}
.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;
min-width: 0;
overflow: hidden;
}
.app-layout-chat .main-content-inner {
overflow: hidden;
min-width: 0;
}
/* Footer */
.app-footer {
background: transparent;
border-top: 1px solid var(--color-border-divider);
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 {
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 {
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;
font-size: var(--text-xs);
color: var(--color-text-muted);
text-decoration: none;
transition: color 150ms;
}
.app-footer-links a:hover {
color: var(--color-text-secondary);
}
.app-footer-copyright {
font-size: var(--text-xs);
color: var(--color-text-muted);
}
.app-footer-copyright a {
color: var(--color-text-muted);
text-decoration: none;
transition: color 150ms;
}
.app-footer-copyright a:hover {
color: var(--color-text-secondary);
}
/* 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);
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;
}
/* 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);
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);
will-change: transform;
}
.sidebar-overlay {
display: none;
}
.sidebar-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--spacing-sm) var(--spacing-sm);
border-bottom: 1px solid var(--color-border-subtle);
min-height: 44px;
}
.sidebar-logo-link {
display: block;
}
.sidebar-logo-img {
width: 100%;
max-width: 120px;
height: auto;
padding: 0 var(--spacing-xs);
}
.sidebar-logo-icon {
display: none;
}
.sidebar-logo-icon-img {
width: 28px;
height: 28px;
}
.sidebar-close-btn {
display: none;
background: none;
border: none;
color: var(--color-text-secondary);
font-size: 1.25rem;
cursor: pointer;
}
.sidebar-nav {
flex: 1;
padding: 2px 0;
overflow-y: auto;
}
.sidebar-section {
padding: 2px 0;
}
.sidebar-section-title {
padding: var(--spacing-sm) var(--spacing-sm) var(--spacing-xs);
font-size: var(--text-xs);
font-weight: var(--font-weight-semibold);
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--color-text-secondary);
white-space: nowrap;
overflow: hidden;
}
.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 {
color: var(--color-text-primary);
}
.sidebar-section-chevron {
font-size: 0.5rem;
opacity: 0.6;
transition: transform var(--duration-fast), opacity var(--duration-fast);
flex-shrink: 0;
}
.sidebar-section-toggle:hover .sidebar-section-chevron {
opacity: 1;
}
.sidebar-section-toggle.open .sidebar-section-chevron {
transform: rotate(90deg);
}
.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;
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);
color: var(--color-text-secondary);
text-decoration: none;
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);
white-space: nowrap;
overflow: hidden;
}
.nav-item:hover:not(.active) {
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);
}
.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);
}
.nav-icon {
width: 18px;
text-align: center;
flex-shrink: 0;
font-size: 0.85rem;
}
.nav-label {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
transition: opacity 150ms ease;
}
.nav-external {
font-size: 0.55rem;
margin-left: auto;
opacity: 0.5;
flex-shrink: 0;
}
.sidebar-footer {
padding: var(--spacing-xs) var(--spacing-sm);
border-top: 1px solid var(--color-border-subtle);
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-xs);
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;
}
.sidebar-collapse-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
border-radius: var(--radius-md);
font-size: var(--text-sm);
transition: color var(--duration-fast), background var(--duration-fast);
flex-shrink: 0;
}
.sidebar-collapse-btn:hover {
color: var(--color-text-primary);
background: var(--color-surface-hover);
}
/* 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;
}
.sidebar.collapsed .sidebar-section-chevron {
display: none;
}
.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;
}
/* 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); }
}
/* 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;
}
.operation-item {
display: flex;
align-items: center;
gap: var(--spacing-md);
padding: var(--spacing-xs) 0;
}
.operation-info {
display: flex;
align-items: center;
gap: var(--spacing-sm);
flex: 2 1 0;
min-width: 0;
}
.operation-info > .operation-text {
flex: 1 1 auto;
min-width: 0;
}
.operation-spinner {
width: 16px;
height: 16px;
flex-shrink: 0;
box-sizing: border-box;
border: 2px solid var(--color-border-default);
border-top-color: var(--color-primary);
border-radius: 50%;
animation: spin 0.8s linear infinite;
display: inline-block;
}
.operation-text {
font-size: 0.8125rem;
color: var(--color-text-secondary);
overflow: hidden;
text-overflow: ellipsis;
}
.operation-progress {
font-size: 0.75rem;
color: var(--color-primary);
font-weight: 500;
}
.operation-bar-container {
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;
background: var(--color-surface-sunken);
border-radius: var(--radius-full);
overflow: hidden;
}
.operation-bar {
height: 100%;
background: var(--color-primary);
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;
}
/* 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;
}
.operation-cancel {
flex-shrink: 0;
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
padding: 4px 6px;
font-size: 0.875rem;
}
.operation-cancel:hover {
color: var(--color-error);
}
/* Toast */
.toast-container {
position: fixed;
top: var(--spacing-lg);
right: var(--spacing-lg);
z-index: 1100;
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);
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);
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);
}
.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);
}
.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);
}
.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);
}
.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);
}
.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);
}
.toast-close {
margin-left: auto;
background: none;
border: none;
color: inherit;
opacity: 0.6;
cursor: pointer;
padding: 2px;
}
.toast-close:hover { opacity: 1; }
.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;
}
/* 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);
}
/* 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). */
.page {
padding: var(--spacing-xl) var(--spacing-xl) var(--spacing-2xl);
width: 100%;
max-width: var(--page-max, var(--page-max-default));
margin: 0 auto;
animation: fadeIn var(--duration-normal) var(--ease-default);
}
.page--narrow { --page-max: var(--page-max-narrow); }
.page--medium { --page-max: var(--page-max-medium); }
.page--wide { --page-max: var(--page-max-wide); }
.page-header {
margin-bottom: var(--spacing-xl);
}
.page-title {
font-size: var(--text-2xl);
font-weight: var(--font-weight-semibold);
letter-spacing: -0.015em;
line-height: var(--leading-tight);
margin-bottom: var(--spacing-xs);
color: var(--color-text-primary);
}
.page-subtitle {
font-size: var(--text-sm);
color: var(--color-text-secondary);
line-height: var(--leading-normal);
}
/* 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);
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);
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);
}
.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);
transform: translateY(-1px);
}
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);
}
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
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);
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;
gap: var(--spacing-md);
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;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--spacing-xs);
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;
border-radius: var(--radius-md);
font-size: var(--text-sm);
font-family: inherit;
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;
cursor: pointer;
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);
text-decoration: none;
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);
}
/* 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);
border-radius: var(--radius-sm);
}
.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);
}
.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);
}
.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);
color: var(--color-text-primary);
border-color: var(--color-border-default);
}
.btn-secondary:hover:not(:disabled) {
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);
}
.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);
color: var(--color-text-primary);
}
.btn-danger {
background: var(--color-error-light);
color: var(--color-error);
border-color: var(--color-error-border);
}
.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);
}
.btn-sm {
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;
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;
}
.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);
}
.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;
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;
}
/* 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 */
.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);
color: var(--color-text-primary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--text-sm);
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;
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);
}
.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;
}
.input:hover:not(:disabled):not(:focus) {
border-color: var(--color-border-strong);
}
.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);
}
.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;
}
select.input {
cursor: pointer;
padding-right: var(--spacing-xl);
}
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;
}
.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);
color: var(--color-text-primary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--text-sm);
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;
outline: none;
width: 100%;
resize: vertical;
min-height: 80px;
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);
}
.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;
}
.textarea:hover:not(:disabled):not(:focus) {
border-color: var(--color-border-strong);
}
.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);
}
.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;
}
/* CodeMirror editor wrapper */
.code-editor-cm .cm-editor {
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
}
.code-editor-cm .cm-editor.cm-focused {
border-color: var(--color-border-strong);
outline: none;
}
/* 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 */
.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;
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;
}
.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);
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;
}
/* 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);
transition: background var(--duration-fast) var(--ease-default);
}
.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;
}
/* 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);
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);
}
/* 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 {
padding: 6px var(--spacing-md);
background: var(--color-surface-raised);
color: var(--color-text-secondary);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-full);
cursor: pointer;
font-size: var(--text-xs);
font-weight: var(--font-weight-medium);
font-family: inherit;
box-shadow: var(--shadow-subtle);
transition: all var(--duration-fast);
}
.filter-btn:hover {
border-color: var(--color-primary-border);
color: var(--color-primary);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
.filter-btn.active {
background: var(--color-primary-light);
color: var(--color-primary);
border-color: var(--color-primary-border);
}
/* 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);
}
.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);
}
/* 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. */
.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);
}
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%; }
.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;
}
.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;
}
.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);
}
/* 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); }
}
@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); }
}
@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); }
}
@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;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
min-width: 0;
}
/* Chat-specific styles */
.chat-layout {
display: flex;
flex: 1;
min-height: 0;
min-width: 0;
overflow: hidden;
position: relative;
}
.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;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius-md);
cursor: pointer;
font-size: var(--text-sm);
color: var(--color-text-secondary);
border-left: 3px solid transparent;
transition: background var(--duration-fast), color var(--duration-fast), border-color var(--duration-fast);
margin-bottom: 2px;
}
.chat-list-item:hover:not(.active) {
background: var(--color-surface-hover);
color: var(--color-text-primary);
}
.chat-list-item.active {
background: var(--color-primary-light);
color: var(--color-primary);
border-left-color: var(--color-primary);
font-weight: var(--font-weight-medium);
}
.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;
overflow: hidden;
}
.chat-messages {
flex: 1;
overflow-y: auto;
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);
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);
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);
}
.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%;
min-width: 0;
animation: messageSlideIn 250ms ease-out;
}
.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);
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;
flex-shrink: 0;
}
.chat-message-user .chat-message-avatar {
background: var(--color-primary);
color: var(--color-primary-text);
}
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. */
.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;
}
.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);
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;
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;
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);
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);
}
.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);
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);
}
.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);
}
.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);
}
.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);
text-decoration: underline;
text-underline-offset: 2px;
}
.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);
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);
}
.chat-message-system {
align-self: center;
max-width: 90%;
}
.chat-message-system .chat-message-bubble {
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);
}
.chat-message-timestamp {
font-size: 0.6875rem;
color: var(--color-text-muted);
margin-top: 2px;
}
.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);
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;
background: var(--color-surface-raised);
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);
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;
}
}
.chat-input-wrapper:focus-within {
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);
}
.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);
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;
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;
}
.chat-input::placeholder {
color: var(--color-text-muted);
}
.chat-send-btn {
padding: var(--spacing-xs);
background: var(--color-primary);
color: var(--color-primary-text);
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;
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;
transition: background var(--duration-fast), transform var(--duration-fast);
}
.chat-send-btn:hover:not(:disabled) {
background: var(--color-primary-hover);
}
.chat-send-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.chat-send-btn:active:not(:disabled) {
transform: scale(0.92);
}
.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;
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;
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;
}
/* 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);
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; }
}
/* 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;
}
/* 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; }
}
/* 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);
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);
}
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; }
.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;
}
.chat-empty-suggestions {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-sm);
justify-content: center;
margin-bottom: var(--spacing-lg);
max-width: 520px;
}
.chat-empty-suggestion {
padding: 8px var(--spacing-md);
background: var(--color-surface-raised);
border: 1px solid var(--color-border-default);
border-radius: var(--radius-full);
font-size: var(--text-sm);
font-weight: var(--font-weight-medium);
font-family: inherit;
color: var(--color-text-secondary);
cursor: pointer;
box-shadow: var(--shadow-subtle), var(--shadow-inset-top);
transition: all var(--duration-fast);
}
.chat-empty-suggestion:hover {
border-color: var(--color-primary-border);
color: var(--color-primary);
background: var(--color-surface-raised);
transform: translateY(-1px);
box-shadow: var(--shadow-sm), var(--shadow-inset-top);
}
.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);
}
/* Activity group (thinking + tools collapsed into one line) */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.chat-activity-group {
display: flex;
flex-direction: column;
min-width: 0;
overflow: hidden;
border-left: 2px solid var(--color-border-subtle);
}
.chat-activity-streaming {
border-left-color: var(--color-primary);
}
.chat-activity-toggle {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-sm);
padding: 6px 12px;
background: none;
border: none;
cursor: pointer;
font-family: inherit;
color: var(--color-text-muted);
transition: color 150ms;
width: 100%;
text-align: left;
}
.chat-activity-toggle:hover {
color: var(--color-text-secondary);
}
.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;
font-weight: 600;
color: var(--color-text-muted);
opacity: 0.6;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.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;
overflow-y: auto;
overflow-x: hidden;
line-height: 1.5;
word-break: break-word;
overflow-wrap: anywhere;
min-width: 0;
}
.chat-activity-item-content.chat-activity-live {
max-height: 300px;
}
.chat-activity-item-content p { margin: 0 0 4px; }
.chat-activity-item-content p:last-child { margin-bottom: 0; }
.chat-activity-item-content pre {
background: var(--color-bg-tertiary);
padding: var(--spacing-xs);
border-radius: var(--radius-sm);
overflow-x: auto;
font-size: 0.75rem;
white-space: pre-wrap;
word-break: break-word;
}
.chat-activity-item-content code {
word-break: break-word;
overflow-wrap: anywhere;
}
.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;
}
.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);
font-size: 0.65rem;
}
.chat-activity-params {
display: flex;
flex-direction: column;
gap: 3px;
margin-top: 2px;
}
.chat-activity-param {
display: flex;
gap: 6px;
font-size: 0.675rem;
line-height: 1.4;
word-break: break-word;
}
.chat-activity-param-key {
color: var(--color-text-muted);
flex-shrink: 0;
opacity: 0.7;
}
.chat-activity-param-val {
color: var(--color-text-secondary);
white-space: pre-wrap;
word-break: break-word;
min-width: 0;
}
.chat-activity-param-val-long {
max-height: 80px;
overflow-y: auto;
}
.chat-activity-thinking {
border-left-color: var(--color-info-border);
}
.chat-activity-tool-call {
border-left-color: var(--color-warning-border);
}
.chat-activity-tool-result {
border-left-color: var(--color-success-border);
}
/* 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);
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);
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;
}
.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;
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;
}
.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;
}
.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; }
}
/* Chat MCP dropdown */
.chat-mcp-dropdown {
position: relative;
display: inline-block;
}
.chat-mcp-dropdown .btn {
display: flex;
align-items: center;
gap: 5px;
}
.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);
left: 0;
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);
animation: dropdownIn 120ms ease-out;
}
.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);
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text-secondary);
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 {
background: var(--color-success);
}
.chat-client-mcp-status-connecting {
background: var(--color-warning);
animation: pulse 1s infinite;
}
.chat-client-mcp-status-error {
background: var(--color-error);
}
.chat-client-mcp-status-disconnected {
background: var(--color-text-tertiary);
}
/* 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);
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);
will-change: transform;
}
.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 {
background: rgba(59, 130, 246, 0.15);
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;
background: var(--color-border-default);
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;
background: color-mix(in oklab, currentColor 12%, transparent);
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;
}
/* Studio tabs */
.studio-tabs {
display: flex;
gap: var(--spacing-xs);
border-bottom: 1px solid var(--color-border-subtle);
padding: var(--spacing-sm) var(--spacing-xl) 0;
background: var(--color-bg-primary);
position: sticky;
top: 0;
z-index: 10;
}
.studio-tab {
display: flex;
align-items: center;
gap: var(--spacing-xs);
background: none;
border: none;
padding: 10px var(--spacing-md);
font-size: var(--text-sm);
font-family: inherit;
font-weight: var(--font-weight-medium);
color: var(--color-text-secondary);
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
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);
font-weight: var(--font-weight-semibold);
}
/* Two-column layout for media generation pages */
.media-layout {
display: grid;
grid-template-columns: minmax(320px, 420px) 1fr;
gap: var(--spacing-lg);
padding: var(--spacing-xl);
max-width: var(--page-max-default);
margin: 0 auto;
width: 100%;
align-items: start;
}
@media (max-width: 900px) {
.media-layout {
grid-template-columns: 1fr;
}
}
.media-controls {
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);
position: sticky;
top: var(--spacing-lg);
}
.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;
}
.media-controls .page-header {
margin-bottom: var(--spacing-lg);
padding-bottom: var(--spacing-md);
border-bottom: 1px solid var(--color-border-divider);
}
.media-controls .page-title {
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);
}
.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%;
}
/* 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);
}
/* ============================================================
Responsive
------------------------------------------------------------
Three viewport tiers, expressed as cascading media queries:
* desktop (1024px) full sidebar, content margin-left = sidebar-width
* tablet (6401023) 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) */
@media (max-width: 1023px) {
.hamburger-btn {
min-width: 44px;
min-height: 44px;
padding: 10px 12px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.nav-item {
min-height: 44px;
}
.sidebar-close-btn {
min-width: 44px;
min-height: 44px;
align-items: center;
justify-content: center;
padding: 10px;
}
.mobile-header {
min-height: 56px;
}
/* Layouts that need to collapse to single-column on any narrow viewport */
.chat-sidebar { display: none; }
.chat-settings-drawer {
width: 100%;
max-width: 100%;
}
.media-layout { grid-template-columns: 1fr; }
.media-controls { position: static; }
.page { padding: var(--spacing-md); }
/* The desktop collapse chevron is desktop-only tablets auto-rail,
mobile uses the hamburger. */
.sidebar-collapse-btn { display: none; }
}
/* Tablet (6401023): 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);
}
.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);
}
.sidebar:not(.open) .sidebar-user-name,
.sidebar:not(.open) .sidebar-logout-btn { display: none; }
/* Pinned open: overlay the full sidebar on top of content */
.sidebar.open {
width: var(--sidebar-width);
box-shadow: var(--shadow-md);
}
.sidebar-close-btn { display: none; }
.sidebar.open .sidebar-close-btn { display: flex; }
.sidebar-overlay {
display: block;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 40;
}
}
/* Mobile (<640): sidebar slides off-screen as a drawer */
@media (max-width: 639px) {
.main-content,
.sidebar-is-collapsed .main-content {
margin-left: 0;
}
.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; }
.sidebar.collapsed .nav-item {
justify-content: flex-start;
padding: 10px var(--spacing-md);
border-left-width: 3px;
}
.sidebar.collapsed .nav-icon {
width: 18px;
font-size: 0.85rem;
}
.sidebar.collapsed .sidebar-header { justify-content: space-between; }
.sidebar-overlay {
display: block;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 40;
}
}
/* 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);
}
/* 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;
}
.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));
}
/* Operations toasts: scroll horizontally instead of wrapping */
.operations-bar {
overflow-x: auto;
flex-wrap: nowrap;
-webkit-overflow-scrolling: touch;
}
.operation-item { flex-shrink: 0; }
.operation-text {
max-width: 60vw;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
/* 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;
}
}
/* 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;
}
}
@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);
}
.chat-empty-suggestions {
flex-direction: column;
align-items: stretch;
}
}
/* 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);
}
/* 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);
}
.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);
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);
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 {
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);
}
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);
}
/* Home page */
.home-page {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
max-width: 52rem;
margin: 0 auto;
padding: var(--spacing-2xl) var(--spacing-xl);
width: 100%;
gap: var(--spacing-md);
}
.home-hero {
text-align: center;
padding: var(--spacing-sm) 0 var(--spacing-md);
}
.home-logo {
width: 72px;
height: auto;
margin: 0 auto;
display: block;
}
/* Home resource bar - prominent */
.home-resource-bar {
width: 100%;
max-width: 420px;
padding: var(--spacing-md) var(--spacing-lg);
background: var(--color-surface-raised);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-subtle);
}
.home-resource-bar-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: var(--text-sm);
color: var(--color-text-secondary);
margin-bottom: var(--spacing-sm);
}
.home-resource-label {
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
}
.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);
font-weight: var(--font-weight-medium);
font-size: var(--text-xs);
}
.home-resource-track {
width: 100%;
height: 6px;
background: var(--color-surface-sunken);
border-radius: var(--radius-full);
overflow: hidden;
}
.home-resource-fill {
height: 100%;
border-radius: var(--radius-full);
transition: width 500ms ease;
}
.home-cluster-status {
display: flex;
align-items: center;
gap: var(--spacing-xs);
font-size: var(--text-xs);
color: var(--color-text-muted);
margin-top: var(--spacing-sm);
}
.home-cluster-dot {
width: 6px;
height: 6px;
border-radius: var(--radius-full);
background: var(--color-success);
display: inline-block;
}
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;
}
}
/* Home chat card */
.home-chat-card {
width: 100%;
background: var(--color-surface-raised);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-xl);
padding: var(--spacing-lg);
box-shadow: var(--shadow-md);
}
.home-model-row {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
}
.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;
gap: 6px;
padding: 4px 10px;
background: var(--color-surface-sunken);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-full);
font-size: var(--text-xs);
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;
}
.home-file-tag button:hover {
color: var(--color-error);
}
/* Home input container */
.home-input-container {
background: var(--color-surface-sunken);
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 {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px var(--color-primary-light);
}
.home-textarea {
width: 100%;
background: transparent;
color: var(--color-text-primary);
border: none;
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
padding: var(--spacing-md);
font-size: var(--text-base);
font-family: inherit;
outline: none;
resize: none;
min-height: 84px;
line-height: var(--leading-normal);
}
.home-textarea::placeholder {
color: var(--color-text-muted);
}
.home-input-footer {
display: flex;
align-items: center;
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);
}
.home-attach-buttons {
display: flex;
gap: 2px;
}
.home-attach-btn {
background: none;
border: none;
color: var(--color-text-muted);
cursor: pointer;
padding: 6px 8px;
font-size: var(--text-sm);
border-radius: var(--radius-md);
transition: color var(--duration-fast), background var(--duration-fast);
}
.home-attach-btn:hover {
color: var(--color-primary);
background: var(--color-primary-light);
}
.home-input-hint {
flex: 1;
text-align: center;
font-size: var(--text-xs);
color: var(--color-text-muted);
letter-spacing: 0.02em;
}
.home-send-btn {
display: flex;
align-items: center;
justify-content: center;
width: 34px;
height: 34px;
background: var(--color-primary);
color: var(--color-primary-text);
border: none;
border-radius: var(--radius-full);
font-size: var(--text-sm);
cursor: pointer;
box-shadow: var(--shadow-sm);
transition: background var(--duration-fast), transform 100ms, box-shadow var(--duration-fast);
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);
padding: 8px var(--spacing-md);
background: var(--color-surface-raised);
color: var(--color-text-secondary);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-full);
font-size: var(--text-xs);
font-weight: var(--font-weight-medium);
font-family: inherit;
cursor: pointer;
text-decoration: none;
box-shadow: var(--shadow-subtle);
transition: all var(--duration-fast);
}
.home-link-btn:hover {
border-color: var(--color-primary-border);
color: var(--color-primary);
transform: translateY(-1px);
box-shadow: var(--shadow-sm);
}
/* Home loaded models */
.home-loaded-models {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
background: var(--color-surface-raised);
border: 1px solid var(--color-border-subtle);
border-radius: var(--radius-lg);
font-size: var(--text-xs);
color: var(--color-text-secondary);
width: 100%;
box-shadow: var(--shadow-subtle);
}
.home-loaded-dot {
width: 8px;
height: 8px;
border-radius: var(--radius-full);
background: var(--color-success);
}
.home-loaded-text {
font-weight: var(--font-weight-medium);
color: var(--color-text-primary);
}
.home-loaded-list {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
}
.home-loaded-item {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 3px 10px;
background: var(--color-surface-sunken);
border: 1px solid var(--color-border-divider);
border-radius: var(--radius-full);
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);
}
.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 {
font-size: var(--text-2xl);
font-weight: var(--font-weight-semibold);
letter-spacing: -0.015em;
margin-bottom: var(--spacing-sm);
color: var(--color-text-primary);
}
.home-wizard-hero p {
color: var(--color-text-secondary);
font-size: var(--text-base);
line-height: var(--leading-normal);
}
.home-wizard-steps {
margin-bottom: var(--spacing-xl);
}
.home-wizard-steps h2 {
font-size: var(--text-lg);
font-weight: var(--font-weight-semibold);
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);
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;
}
/* 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;
}
}