mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
feat: add skin logo support
This commit is contained in:
parent
af077b2c0d
commit
ebd2d83ef2
6 changed files with 108 additions and 13 deletions
|
|
@ -205,7 +205,13 @@ def resolve_skin() -> dict:
|
|||
from hermes_cli.skin_engine import init_skin_from_config, get_active_skin
|
||||
init_skin_from_config(_load_cfg())
|
||||
skin = get_active_skin()
|
||||
return {"name": skin.name, "colors": skin.colors, "branding": skin.branding}
|
||||
return {
|
||||
"name": skin.name,
|
||||
"colors": skin.colors,
|
||||
"branding": skin.branding,
|
||||
"banner_logo": skin.banner_logo,
|
||||
"banner_hero": skin.banner_hero,
|
||||
}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
|
|
|||
11
ui-tui/package-lock.json
generated
11
ui-tui/package-lock.json
generated
|
|
@ -73,6 +73,7 @@
|
|||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
|
|
@ -1092,6 +1093,7 @@
|
|||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
|
|
@ -1102,6 +1104,7 @@
|
|||
"integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.58.0",
|
||||
|
|
@ -1131,6 +1134,7 @@
|
|||
"integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.58.0",
|
||||
"@typescript-eslint/types": "8.58.0",
|
||||
|
|
@ -1335,6 +1339,7 @@
|
|||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -1647,6 +1652,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.10.12",
|
||||
"caniuse-lite": "^1.0.30001782",
|
||||
|
|
@ -2313,6 +2319,7 @@
|
|||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
|
|
@ -4238,6 +4245,7 @@
|
|||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -4308,6 +4316,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
|
|
@ -5060,6 +5069,7 @@
|
|||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -5351,6 +5361,7 @@
|
|||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -476,7 +476,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
|||
switch (ev.type) {
|
||||
case 'gateway.ready':
|
||||
if (p?.skin) {
|
||||
setTheme(fromSkin(p.skin.colors ?? {}, p.skin.branding ?? {}))
|
||||
setTheme(fromSkin(p.skin.colors ?? {}, p.skin.branding ?? {}, p.skin.banner_logo ?? '', p.skin.banner_hero ?? ''))
|
||||
}
|
||||
|
||||
rpc('commands.catalog', {})
|
||||
|
|
|
|||
|
|
@ -2,6 +2,50 @@ import type { ThemeColors } from './theme.js'
|
|||
|
||||
type Line = [string, string]
|
||||
|
||||
// ── Rich markup parser ──────────────────────────────────────────────
|
||||
// Parses Python Rich markup like "[bold #A3261F]text[/]" into Line[].
|
||||
const RICH_RE = /\[(?:bold\s+)?(?:dim\s+)?(#(?:[0-9a-fA-F]{3,8}))\]([\s\S]*?)(\[\/\])/g
|
||||
|
||||
export function parseRichMarkup(markup: string): Line[] {
|
||||
const lines: Line[] = []
|
||||
|
||||
for (const raw of markup.split('\n')) {
|
||||
const trimmed = raw.trimEnd()
|
||||
|
||||
if (!trimmed) {
|
||||
lines.push(['', ' '])
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
let lastIndex = 0
|
||||
let matched = false
|
||||
let m: RegExpExecArray | null
|
||||
|
||||
RICH_RE.lastIndex = 0
|
||||
|
||||
while ((m = RICH_RE.exec(trimmed)) !== null) {
|
||||
matched = true
|
||||
const before = trimmed.slice(lastIndex, m.index)
|
||||
|
||||
if (before) {
|
||||
lines.push(['', before])
|
||||
}
|
||||
|
||||
lines.push([m[1]!, m[2]!])
|
||||
lastIndex = m.index + m[0].length
|
||||
}
|
||||
|
||||
if (!matched) {
|
||||
lines.push(['', trimmed])
|
||||
} else if (lastIndex < trimmed.length) {
|
||||
lines.push(['', trimmed.slice(lastIndex)])
|
||||
}
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
const LOGO_ART = [
|
||||
'██╗ ██╗███████╗██████╗ ███╗ ███╗███████╗███████╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗',
|
||||
'██║ ██║██╔════╝██╔══██╗████╗ ████║██╔════╝██╔════╝ ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝',
|
||||
|
|
@ -39,6 +83,22 @@ function colorize(art: string[], gradient: readonly number[], c: ThemeColors): L
|
|||
}
|
||||
|
||||
export const LOGO_WIDTH = 98
|
||||
export const CADUCEUS_WIDTH = 30
|
||||
|
||||
export const logo = (c: ThemeColors) => colorize(LOGO_ART, LOGO_GRADIENT, c)
|
||||
export const caduceus = (c: ThemeColors) => colorize(CADUCEUS_ART, CADUC_GRADIENT, c)
|
||||
export const logo = (c: ThemeColors, customLogo?: string): Line[] =>
|
||||
customLogo ? parseRichMarkup(customLogo) : colorize(LOGO_ART, LOGO_GRADIENT, c)
|
||||
|
||||
export const caduceus = (c: ThemeColors, customHero?: string): Line[] =>
|
||||
customHero ? parseRichMarkup(customHero) : colorize(CADUCEUS_ART, CADUC_GRADIENT, c)
|
||||
|
||||
export function artWidth(lines: Line[]): number {
|
||||
let max = 0
|
||||
|
||||
for (const [, text] of lines) {
|
||||
if (text.length > max) {
|
||||
max = text.length
|
||||
}
|
||||
}
|
||||
|
||||
return max
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Box, Text, useStdout } from 'ink'
|
||||
|
||||
import { caduceus, logo, LOGO_WIDTH } from '../banner.js'
|
||||
import { artWidth, caduceus, CADUCEUS_WIDTH, logo, LOGO_WIDTH } from '../banner.js'
|
||||
import { flat } from '../lib/text.js'
|
||||
import type { Theme } from '../theme.js'
|
||||
import type { SessionInfo } from '../types.js'
|
||||
|
|
@ -19,16 +19,19 @@ export function ArtLines({ lines }: { lines: [string, string][] }) {
|
|||
|
||||
export function Banner({ t }: { t: Theme }) {
|
||||
const cols = useStdout().stdout?.columns ?? 80
|
||||
const logoLines = logo(t.color, t.bannerLogo || undefined)
|
||||
const logoW = t.bannerLogo ? artWidth(logoLines) : LOGO_WIDTH
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
{cols >= LOGO_WIDTH ? (
|
||||
<ArtLines lines={logo(t.color)} />
|
||||
{cols >= logoW ? (
|
||||
<ArtLines lines={logoLines} />
|
||||
) : (
|
||||
<Text bold color={t.color.gold}>
|
||||
{t.brand.icon} NOUS HERMES
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text color={t.color.dim}>{t.brand.icon} Nous Research · Messenger of the Digital Gods</Text>
|
||||
</Box>
|
||||
)
|
||||
|
|
@ -36,8 +39,10 @@ export function Banner({ t }: { t: Theme }) {
|
|||
|
||||
export function SessionPanel({ info, sid, t }: { info: SessionInfo; sid?: string | null; t: Theme }) {
|
||||
const cols = useStdout().stdout?.columns ?? 100
|
||||
const wide = cols >= 90
|
||||
const leftW = wide ? 34 : 0
|
||||
const heroLines = caduceus(t.color, t.bannerHero || undefined)
|
||||
const heroW = artWidth(heroLines) || CADUCEUS_WIDTH
|
||||
const leftW = Math.min(heroW + 4, Math.floor(cols * 0.4))
|
||||
const wide = cols >= 90 && leftW + 40 < cols
|
||||
const w = wide ? cols - leftW - 12 : cols - 10
|
||||
const cwd = info.cwd || process.cwd()
|
||||
const strip = (s: string) => (s.endsWith('_tools') ? s.slice(0, -6) : s)
|
||||
|
|
@ -88,7 +93,7 @@ export function SessionPanel({ info, sid, t }: { info: SessionInfo; sid?: string
|
|||
<Box borderColor={t.color.bronze} borderStyle="round" marginBottom={1} paddingX={2} paddingY={1}>
|
||||
{wide && (
|
||||
<Box flexDirection="column" marginRight={2} width={leftW}>
|
||||
<ArtLines lines={caduceus(t.color)} />
|
||||
<ArtLines lines={heroLines} />
|
||||
<Text />
|
||||
<Text color={t.color.amber}>
|
||||
{info.model.split('/').pop()}
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ export interface ThemeBrand {
|
|||
export interface Theme {
|
||||
color: ThemeColors
|
||||
brand: ThemeBrand
|
||||
bannerLogo: string
|
||||
bannerHero: string
|
||||
}
|
||||
|
||||
export const DEFAULT_THEME: Theme = {
|
||||
|
|
@ -60,10 +62,18 @@ export const DEFAULT_THEME: Theme = {
|
|||
welcome: 'Type your message or /help for commands.',
|
||||
goodbye: 'Goodbye! ⚕',
|
||||
tool: '┊'
|
||||
}
|
||||
},
|
||||
|
||||
bannerLogo: '',
|
||||
bannerHero: ''
|
||||
}
|
||||
|
||||
export function fromSkin(colors: Record<string, string>, branding: Record<string, string>): Theme {
|
||||
export function fromSkin(
|
||||
colors: Record<string, string>,
|
||||
branding: Record<string, string>,
|
||||
bannerLogo = '',
|
||||
bannerHero = ''
|
||||
): Theme {
|
||||
const d = DEFAULT_THEME
|
||||
const c = (k: string) => colors[k]
|
||||
|
||||
|
|
@ -95,6 +105,9 @@ export function fromSkin(colors: Record<string, string>, branding: Record<string
|
|||
welcome: branding.welcome ?? d.brand.welcome,
|
||||
goodbye: branding.goodbye ?? d.brand.goodbye,
|
||||
tool: d.brand.tool
|
||||
}
|
||||
},
|
||||
|
||||
bannerLogo,
|
||||
bannerHero
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue