mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-28 18:19:28 +00:00
refactor(ui-tui): DRY the chrome primitives — shared scrollbarColors + hintRgb
scrollbarColors(t, hover, grabbed) in overlayPrimitives is now THE scheme for both scrollbars (was duplicated formulas); textInput's hex parsing collapses into one hintRgb helper feeding colorizeHint and hintCursorCell. Comment bloat trimmed. No behavior change — 1243 tests byte-green.
This commit is contained in:
parent
505b2de484
commit
a0a9aaf8b8
5 changed files with 44 additions and 52 deletions
|
|
@ -11,13 +11,14 @@ import { FACES } from '../content/faces.js'
|
|||
import { VERBS } from '../content/verbs.js'
|
||||
import { fmtDuration } from '../domain/messages.js'
|
||||
import { stickyPromptFromViewport } from '../domain/viewport.js'
|
||||
import { mix } from '../lib/color.js'
|
||||
import { buildSubagentTree, treeTotals, widthByDepth } from '../lib/subagentTree.js'
|
||||
import { fmtK } from '../lib/text.js'
|
||||
import { useScrollbarSnapshot, useViewportSnapshot } from '../lib/viewportStore.js'
|
||||
import type { Theme } from '../theme.js'
|
||||
import type { Msg, Usage } from '../types.js'
|
||||
|
||||
import { scrollbarColors } from './overlayPrimitives.js'
|
||||
|
||||
const FACE_TICK_MS = 2500
|
||||
const HEART_COLORS = ['#ff5fa2', '#ff4d6d']
|
||||
|
||||
|
|
@ -754,14 +755,7 @@ export function TranscriptScrollbar({ scrollRef, t }: TranscriptScrollbarProps)
|
|||
const thumb = scrollable ? Math.max(1, Math.round((vp * vp) / total)) : vp
|
||||
const travel = Math.max(1, vp - thumb)
|
||||
const thumbTop = scrollable ? Math.round((pos / Math.max(1, total - vp)) * travel) : 0
|
||||
// Thumb rides in the theme's BASE color (primary), brightening to accent
|
||||
// while hovered/dragged. The track recedes via an EXPLICIT blend toward the
|
||||
// theme surface — never the SGR dim attribute: dim is terminal-interpreted,
|
||||
// and on transparent profiles (terminal.background #00000000) xterm renders
|
||||
// dim cells as half-bright glyphs on an opaque BLACK cell background. That
|
||||
// was the "black bar / black thumb" on the right edge.
|
||||
const thumbColor = grab !== null || hover ? t.color.accent : t.color.primary
|
||||
const trackColor = mix(hover ? t.color.border : t.color.muted, t.color.completionBg, hover ? 0.25 : 0.55)
|
||||
const { thumb: thumbColor, track: trackColor } = scrollbarColors(t, hover, grab !== null)
|
||||
|
||||
const jump = (row: number, offset: number) => {
|
||||
if (!s || !scrollable) {
|
||||
|
|
@ -793,15 +787,10 @@ export function TranscriptScrollbar({ scrollRef, t }: TranscriptScrollbarProps)
|
|||
}}
|
||||
width={1}
|
||||
>
|
||||
{!scrollable ? null : ( // Nothing to scroll → draw nothing. The outer
|
||||
// width={1} Box still reserves the column so the transcript width
|
||||
// doesn't jump when content grows scrollable. Previously this drew a
|
||||
// full-height column of SPACES; on a transparent terminal
|
||||
// (terminal.background #00000000) those drawn-blank cells composite to
|
||||
// a black bar (the "big black area" by the scrollbar) — same class as
|
||||
// the banner's removed opaque fill. A `│` track only appears when
|
||||
// there's actually something to scroll, which is what reads as "makes
|
||||
// sense."
|
||||
{/* Nothing to scroll → draw nothing (the width={1} Box still reserves
|
||||
the column). Drawn-blank cells composite to a black bar on
|
||||
transparent terminals — same class as the removed opaque fills. */}
|
||||
{!scrollable ? null : (
|
||||
<>
|
||||
{thumbTop > 0 ? (
|
||||
<Text color={trackColor}>{`${'│\n'.repeat(Math.max(0, thumbTop - 1))}${thumbTop > 0 ? '│' : ''}`}</Text>
|
||||
|
|
|
|||
|
|
@ -232,12 +232,10 @@ export function SessionPanel({ info, maxWidth, sid, t }: SessionPanelProps) {
|
|||
const lineBudget = Math.max(12, w - 2)
|
||||
const strip = (s: string) => (s.endsWith('_tools') ? s.slice(0, -6) : s)
|
||||
|
||||
// Hierarchy: category labels lead in the theme's label tone; member lists
|
||||
// recede in the midpoint of muted and text. Anchoring on MUTED (a
|
||||
// mid-luminance family tone) keeps the fade readable on both poles even
|
||||
// when polarity detection is wrong — blending toward the SURFACE instead
|
||||
// made the fade invisible whenever text was already pale (light-rendered
|
||||
// default: cream toward white = nothing).
|
||||
// Hierarchy: labels lead in the label tone; member lists recede in the
|
||||
// muted/text midpoint. Anchoring on MUTED (mid-luminance by construction)
|
||||
// keeps the fade readable on both poles even when polarity detection is
|
||||
// wrong — surface-relative blends go invisible when text is already pale.
|
||||
const listFade = mix(t.color.muted, t.color.text, 0.5)
|
||||
|
||||
// ── Local collapse state for each section ──
|
||||
|
|
|
|||
|
|
@ -3,9 +3,22 @@ import { Text, useInput } from '@hermes/ink'
|
|||
import { type ReactNode, useState } from 'react'
|
||||
|
||||
import type { UsageModelData } from '../gatewayTypes.js'
|
||||
import { liftForContrast } from '../lib/color.js'
|
||||
import { liftForContrast, mix } from '../lib/color.js'
|
||||
import type { Theme } from '../theme.js'
|
||||
|
||||
/**
|
||||
* THE scrollbar treatment (transcript + overlays): thumb rides the theme
|
||||
* base, accent while interacting; track recedes via an explicit blend toward
|
||||
* the surface. Never SGR dim — terminal-interpreted, it renders as a black
|
||||
* slab on transparent profiles (terminal.background #00000000).
|
||||
*/
|
||||
export function scrollbarColors(t: Theme, hover: boolean, grabbed: boolean): { thumb: string; track: string } {
|
||||
return {
|
||||
thumb: grabbed || hover ? t.color.accent : t.color.primary,
|
||||
track: mix(hover ? t.color.border : t.color.muted, t.color.completionBg, hover ? 0.25 : 0.55)
|
||||
}
|
||||
}
|
||||
|
||||
export interface MenuRowSpec {
|
||||
color?: string
|
||||
label: string
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { Box, type ScrollBoxHandle, Text } from '@hermes/ink'
|
||||
import { type RefObject, useState } from 'react'
|
||||
|
||||
import { mix } from '../lib/color.js'
|
||||
import type { Theme } from '../theme.js'
|
||||
|
||||
import { scrollbarColors } from './overlayPrimitives.js'
|
||||
|
||||
/**
|
||||
* Mouse-draggable scrollbar bound to a `ScrollBox` ref. Re-renders off the
|
||||
* parent `tick` so accordions / async content can resize the thumb without a
|
||||
|
|
@ -40,11 +41,7 @@ export function OverlayScrollbar({
|
|||
|
||||
const vBar = (n: number) => (n > 0 ? `${'│\n'.repeat(n - 1)}│` : '')
|
||||
const thumbBody = `${'┃\n'.repeat(Math.max(0, thumb - 1))}┃`
|
||||
// Same scheme as TranscriptScrollbar: base-color thumb, accent on interact,
|
||||
// track receded via explicit blend (SGR dim renders as a black slab on
|
||||
// transparent terminals — see TranscriptScrollbar).
|
||||
const thumbColor = grab !== null || hover ? t.color.accent : t.color.primary
|
||||
const trackColor = mix(hover ? t.color.border : t.color.muted, t.color.completionBg, hover ? 0.25 : 0.55)
|
||||
const { thumb: thumbColor, track: trackColor } = scrollbarColors(t, hover, grab !== null)
|
||||
|
||||
const jump = (row: number, offset: number) => {
|
||||
if (!s || !scrollable) {
|
||||
|
|
|
|||
|
|
@ -40,33 +40,28 @@ type MinimalEnv = Record<string, string | undefined>
|
|||
|
||||
const invert = (s: string) => INV + s + INV_OFF
|
||||
|
||||
/** Truecolor foreground wrap. Always emits an EXPLICIT color — never SGR dim
|
||||
* and never inverse: both are terminal-interpreted relative to the default
|
||||
* fg/bg, and on transparent profiles (terminal.background #00000000) they
|
||||
* composite against a black RGB the user never sees elsewhere, rendering the
|
||||
* hint as a black slab. A fixed mid-gray fallback is deterministic on every
|
||||
* host when no theme color is provided. */
|
||||
// Placeholder styling is EXPLICIT truecolor only — never SGR dim/inverse:
|
||||
// both are terminal-interpreted relative to the default fg/bg, and on
|
||||
// transparent profiles (terminal.background #00000000) they composite
|
||||
// against a black RGB the user never sees — the hint rendered as a slab.
|
||||
const HINT_FALLBACK = '#808080'
|
||||
|
||||
const colorizeHint = (s: string, hex?: string) => {
|
||||
const m = /^#([0-9a-f]{6})$/i.exec(hex ?? '') ?? /^#([0-9a-f]{6})$/i.exec(HINT_FALLBACK)!
|
||||
const n = parseInt(m[1]!, 16)
|
||||
const hintRgb = (hex?: string): [number, number, number] => {
|
||||
const n = parseInt((/^#([0-9a-f]{6})$/i.exec(hex ?? '')?.[1] ?? HINT_FALLBACK.slice(1)) as string, 16)
|
||||
|
||||
return `${ESC}[38;2;${(n >> 16) & 0xff};${(n >> 8) & 0xff};${n & 0xff}m${s}${ESC}[39m`
|
||||
return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff]
|
||||
}
|
||||
|
||||
/** Synthetic placeholder cursor: an explicit-truecolor chip (hint-colored
|
||||
* block, luminance-picked ink) instead of SGR inverse. Inverse swaps
|
||||
* against the terminal's DEFAULT fg/bg, which on transparent profiles is a
|
||||
* black RGB the user never sees — the "cursor" rendered as a black slab. */
|
||||
const colorizeHint = (s: string, hex?: string) => {
|
||||
const [r, g, b] = hintRgb(hex)
|
||||
|
||||
return `${ESC}[38;2;${r};${g};${b}m${s}${ESC}[39m`
|
||||
}
|
||||
|
||||
/** Synthetic placeholder cursor: a hint-colored chip with luminance-picked
|
||||
* ink, standing in for the hidden hardware cursor (bubbles pattern). */
|
||||
const hintCursorCell = (ch: string, hex?: string) => {
|
||||
const m = /^#([0-9a-f]{6})$/i.exec(hex ?? '') ?? /^#([0-9a-f]{6})$/i.exec(HINT_FALLBACK)!
|
||||
const n = parseInt(m[1]!, 16)
|
||||
|
||||
const r = (n >> 16) & 0xff,
|
||||
g = (n >> 8) & 0xff,
|
||||
b = n & 0xff
|
||||
|
||||
const [r, g, b] = hintRgb(hex)
|
||||
const ink = 0.2126 * r + 0.7152 * g + 0.0722 * b > 140 ? '0;0;0' : '255;255;255'
|
||||
|
||||
return `${ESC}[48;2;${r};${g};${b}m${ESC}[38;2;${ink}m${ch}${ESC}[39m${ESC}[49m`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue