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
|
from hermes_cli.skin_engine import init_skin_from_config, get_active_skin
|
||||||
init_skin_from_config(_load_cfg())
|
init_skin_from_config(_load_cfg())
|
||||||
skin = get_active_skin()
|
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:
|
except Exception:
|
||||||
return {}
|
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==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
|
|
@ -1092,6 +1093,7 @@
|
||||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
|
|
@ -1102,6 +1104,7 @@
|
||||||
"integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==",
|
"integrity": "sha512-RLkVSiNuUP1C2ROIWfqX+YcUfLaSnxGE/8M+Y57lopVwg9VTYYfhuz15Yf1IzCKgZj6/rIbYTmJCUSqr76r0Wg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.12.2",
|
"@eslint-community/regexpp": "^4.12.2",
|
||||||
"@typescript-eslint/scope-manager": "8.58.0",
|
"@typescript-eslint/scope-manager": "8.58.0",
|
||||||
|
|
@ -1131,6 +1134,7 @@
|
||||||
"integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==",
|
"integrity": "sha512-rLoGZIf9afaRBYsPUMtvkDWykwXwUPL60HebR4JgTI8mxfFe2cQTu3AGitANp4b9B2QlVru6WzjgB2IzJKiCSA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.58.0",
|
"@typescript-eslint/scope-manager": "8.58.0",
|
||||||
"@typescript-eslint/types": "8.58.0",
|
"@typescript-eslint/types": "8.58.0",
|
||||||
|
|
@ -1335,6 +1339,7 @@
|
||||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -1647,6 +1652,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.10.12",
|
"baseline-browser-mapping": "^2.10.12",
|
||||||
"caniuse-lite": "^1.0.30001782",
|
"caniuse-lite": "^1.0.30001782",
|
||||||
|
|
@ -2313,6 +2319,7 @@
|
||||||
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
"integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
|
|
@ -4238,6 +4245,7 @@
|
||||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -4308,6 +4316,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
|
|
@ -5060,6 +5069,7 @@
|
||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|
@ -5351,6 +5361,7 @@
|
||||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -476,7 +476,7 @@ export function App({ gw }: { gw: GatewayClient }) {
|
||||||
switch (ev.type) {
|
switch (ev.type) {
|
||||||
case 'gateway.ready':
|
case 'gateway.ready':
|
||||||
if (p?.skin) {
|
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', {})
|
rpc('commands.catalog', {})
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,50 @@ import type { ThemeColors } from './theme.js'
|
||||||
|
|
||||||
type Line = [string, string]
|
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 = [
|
const LOGO_ART = [
|
||||||
'██╗ ██╗███████╗██████╗ ███╗ ███╗███████╗███████╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗',
|
'██╗ ██╗███████╗██████╗ ███╗ ███╗███████╗███████╗ █████╗ ██████╗ ███████╗███╗ ██╗████████╗',
|
||||||
'██║ ██║██╔════╝██╔══██╗████╗ ████║██╔════╝██╔════╝ ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝',
|
'██║ ██║██╔════╝██╔══██╗████╗ ████║██╔════╝██╔════╝ ██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝',
|
||||||
|
|
@ -39,6 +83,22 @@ function colorize(art: string[], gradient: readonly number[], c: ThemeColors): L
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LOGO_WIDTH = 98
|
export const LOGO_WIDTH = 98
|
||||||
|
export const CADUCEUS_WIDTH = 30
|
||||||
|
|
||||||
export const logo = (c: ThemeColors) => colorize(LOGO_ART, LOGO_GRADIENT, c)
|
export const logo = (c: ThemeColors, customLogo?: string): Line[] =>
|
||||||
export const caduceus = (c: ThemeColors) => colorize(CADUCEUS_ART, CADUC_GRADIENT, c)
|
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 { 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 { flat } from '../lib/text.js'
|
||||||
import type { Theme } from '../theme.js'
|
import type { Theme } from '../theme.js'
|
||||||
import type { SessionInfo } from '../types.js'
|
import type { SessionInfo } from '../types.js'
|
||||||
|
|
@ -19,16 +19,19 @@ export function ArtLines({ lines }: { lines: [string, string][] }) {
|
||||||
|
|
||||||
export function Banner({ t }: { t: Theme }) {
|
export function Banner({ t }: { t: Theme }) {
|
||||||
const cols = useStdout().stdout?.columns ?? 80
|
const cols = useStdout().stdout?.columns ?? 80
|
||||||
|
const logoLines = logo(t.color, t.bannerLogo || undefined)
|
||||||
|
const logoW = t.bannerLogo ? artWidth(logoLines) : LOGO_WIDTH
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" marginBottom={1}>
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
{cols >= LOGO_WIDTH ? (
|
{cols >= logoW ? (
|
||||||
<ArtLines lines={logo(t.color)} />
|
<ArtLines lines={logoLines} />
|
||||||
) : (
|
) : (
|
||||||
<Text bold color={t.color.gold}>
|
<Text bold color={t.color.gold}>
|
||||||
{t.brand.icon} NOUS HERMES
|
{t.brand.icon} NOUS HERMES
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Text color={t.color.dim}>{t.brand.icon} Nous Research · Messenger of the Digital Gods</Text>
|
<Text color={t.color.dim}>{t.brand.icon} Nous Research · Messenger of the Digital Gods</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
|
|
@ -36,8 +39,10 @@ export function Banner({ t }: { t: Theme }) {
|
||||||
|
|
||||||
export function SessionPanel({ info, sid, t }: { info: SessionInfo; sid?: string | null; t: Theme }) {
|
export function SessionPanel({ info, sid, t }: { info: SessionInfo; sid?: string | null; t: Theme }) {
|
||||||
const cols = useStdout().stdout?.columns ?? 100
|
const cols = useStdout().stdout?.columns ?? 100
|
||||||
const wide = cols >= 90
|
const heroLines = caduceus(t.color, t.bannerHero || undefined)
|
||||||
const leftW = wide ? 34 : 0
|
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 w = wide ? cols - leftW - 12 : cols - 10
|
||||||
const cwd = info.cwd || process.cwd()
|
const cwd = info.cwd || process.cwd()
|
||||||
const strip = (s: string) => (s.endsWith('_tools') ? s.slice(0, -6) : s)
|
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}>
|
<Box borderColor={t.color.bronze} borderStyle="round" marginBottom={1} paddingX={2} paddingY={1}>
|
||||||
{wide && (
|
{wide && (
|
||||||
<Box flexDirection="column" marginRight={2} width={leftW}>
|
<Box flexDirection="column" marginRight={2} width={leftW}>
|
||||||
<ArtLines lines={caduceus(t.color)} />
|
<ArtLines lines={heroLines} />
|
||||||
<Text />
|
<Text />
|
||||||
<Text color={t.color.amber}>
|
<Text color={t.color.amber}>
|
||||||
{info.model.split('/').pop()}
|
{info.model.split('/').pop()}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@ export interface ThemeBrand {
|
||||||
export interface Theme {
|
export interface Theme {
|
||||||
color: ThemeColors
|
color: ThemeColors
|
||||||
brand: ThemeBrand
|
brand: ThemeBrand
|
||||||
|
bannerLogo: string
|
||||||
|
bannerHero: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFAULT_THEME: Theme = {
|
export const DEFAULT_THEME: Theme = {
|
||||||
|
|
@ -60,10 +62,18 @@ export const DEFAULT_THEME: Theme = {
|
||||||
welcome: 'Type your message or /help for commands.',
|
welcome: 'Type your message or /help for commands.',
|
||||||
goodbye: 'Goodbye! ⚕',
|
goodbye: 'Goodbye! ⚕',
|
||||||
tool: '┊'
|
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 d = DEFAULT_THEME
|
||||||
const c = (k: string) => colors[k]
|
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,
|
welcome: branding.welcome ?? d.brand.welcome,
|
||||||
goodbye: branding.goodbye ?? d.brand.goodbye,
|
goodbye: branding.goodbye ?? d.brand.goodbye,
|
||||||
tool: d.brand.tool
|
tool: d.brand.tool
|
||||||
}
|
},
|
||||||
|
|
||||||
|
bannerLogo,
|
||||||
|
bannerHero
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue