Initial import of NousResearch/hermes-agent
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
Docker / shell lint / Lint Dockerfile (hadolint) (push) Has been cancelled
Docker / shell lint / Lint docker/ shell scripts (shellcheck) (push) Has been cancelled
Docker Build and Publish / build-amd64 (push) Has been cancelled
Docker Build and Publish / build-arm64 (push) Has been cancelled
Lint (ruff + ty) / ruff + ty diff (push) Has been cancelled
Lint (ruff + ty) / ruff enforcement (blocking) (push) Has been cancelled
Lint (ruff + ty) / Windows footguns (blocking) (push) Has been cancelled
Nix Lockfile Fix / auto-fix-main (push) Has been cancelled
Nix Lockfile Fix / fix (push) Has been cancelled
Nix / nix (macos-latest) (push) Has been cancelled
Nix / nix (ubuntu-latest) (push) Has been cancelled
OSV-Scanner / Scan lockfiles (push) Has been cancelled
Build Skills Index / build-index (push) Has been cancelled
Tests / test (1) (push) Has been cancelled
Tests / test (2) (push) Has been cancelled
Tests / test (3) (push) Has been cancelled
Tests / test (4) (push) Has been cancelled
Tests / test (5) (push) Has been cancelled
Tests / test (6) (push) Has been cancelled
Tests / e2e (push) Has been cancelled
uv.lock check / uv lock --check (push) Has been cancelled
Docker Build and Publish / merge (push) Has been cancelled
Build Skills Index / trigger-deploy (push) Has been cancelled
Tests / save-durations (push) Has been cancelled

This commit is contained in:
红尘
2026-05-31 09:36:58 +08:00
commit d73ff9b0fb
4188 changed files with 1614916 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
import type { DetailsMode, SectionName, SectionVisibility } from '../types.js'
const MODES = ['hidden', 'collapsed', 'expanded'] as const
export const SECTION_NAMES = ['thinking', 'tools', 'subagents', 'activity'] as const
// Out-of-the-box per-section defaults — applied when the user hasn't pinned
// an explicit override and layered ABOVE the global details_mode:
//
// - thinking / tools: expanded — stream open so the turn reads like a
// live transcript (reasoning + tool calls side by side) instead of a
// wall of chevrons the user has to click every turn.
// - activity: hidden — ambient meta (gateway hints, terminal-parity
// nudges, background notifications) is noise for typical use. Tool
// failures still render inline on the failing tool row, and ambient
// errors/warnings surface via the floating-alert backstop when every
// panel resolves to hidden.
// - subagents: not set — falls through to the global details_mode so
// Spawn trees stay under a chevron until a delegation actually happens.
//
// Opt out of any of these with `display.sections.<name>` in config.yaml
// or at runtime via `/details <name> collapsed|hidden`.
const SECTION_DEFAULTS: SectionVisibility = {
thinking: 'expanded',
tools: 'expanded',
activity: 'hidden'
}
const THINKING_FALLBACK: Record<string, DetailsMode> = {
collapsed: 'collapsed',
full: 'expanded',
truncated: 'collapsed'
}
const norm = (v: unknown) =>
String(v ?? '')
.trim()
.toLowerCase()
export const parseDetailsMode = (v: unknown): DetailsMode | null => MODES.find(m => m === norm(v)) ?? null
export const isSectionName = (v: unknown): v is SectionName =>
typeof v === 'string' && (SECTION_NAMES as readonly string[]).includes(v)
export const resolveDetailsMode = (d?: { details_mode?: unknown; thinking_mode?: unknown } | null): DetailsMode =>
parseDetailsMode(d?.details_mode) ?? THINKING_FALLBACK[norm(d?.thinking_mode)] ?? 'collapsed'
// Build SectionVisibility from a free-form blob. Unknown section names and
// invalid modes are dropped silently — partial overrides are intentional, so
// missing keys fall through to SECTION_DEFAULTS / global at lookup time.
export const resolveSections = (raw: unknown): SectionVisibility =>
raw && typeof raw === 'object' && !Array.isArray(raw)
? (Object.fromEntries(
Object.entries(raw as Record<string, unknown>)
.map(([k, v]) => [k, parseDetailsMode(v)] as const)
.filter(([k, m]) => !!m && isSectionName(k))
) as SectionVisibility)
: {}
// Effective mode for one section: explicit override → global command mode →
// built-in live-stream defaults → global config mode.
//
// The `commandOverride` flag is set for in-session `/details <mode>` changes.
// That command should immediately apply to every section, including sections
// with built-in defaults like thinking/tools=expanded and activity=hidden. On
// startup/config sync we keep those defaults layered above the persisted global
// config so the TUI still opens live reasoning/tools by default unless the user
// pins explicit per-section overrides.
export const sectionMode = (
name: SectionName,
global: DetailsMode,
sections?: SectionVisibility,
commandOverride = false
): DetailsMode => sections?.[name] ?? (commandOverride ? global : (SECTION_DEFAULTS[name] ?? global))
export const nextDetailsMode = (m: DetailsMode): DetailsMode => MODES[(MODES.indexOf(m) + 1) % MODES.length]!
+91
View File
@@ -0,0 +1,91 @@
import { LONG_MSG } from '../config/limits.js'
import { buildToolTrailLine, fmtK } from '../lib/text.js'
import type { Msg, SessionInfo } from '../types.js'
export const introMsg = (info: SessionInfo): Msg => ({ info, kind: 'intro', role: 'system', text: '' })
export const imageTokenMeta = (info?: ImageMeta | null) => {
const { width, height, token_estimate: t } = info ?? {}
return [width && height ? `${width}x${height}` : '', (t ?? 0) > 0 ? `~${fmtK(t!)} tok` : '']
.filter(Boolean)
.join(' · ')
}
export const attachedImageNotice = (info?: ({ name?: string } & ImageMeta) | null) => {
const meta = imageTokenMeta(info)
const label = info?.name ? `📎 Attached image: ${info.name}` : '📎 Attached image'
return `${label}${meta ? ` · ${meta}` : ''}`
}
export const userDisplay = (text: string) => {
if (text.length <= LONG_MSG) {
return text
}
const first = text.split('\n')[0]?.trim() ?? ''
const words = first.split(/\s+/).filter(Boolean)
const prefix = (words.length > 1 ? words.slice(0, 4).join(' ') : first).slice(0, 80)
return `${prefix || '(message)'} [long message]`
}
export const toTranscriptMessages = (rows: unknown): Msg[] => {
if (!Array.isArray(rows)) {
return []
}
const out: Msg[] = []
let pending: string[] = []
for (const row of rows) {
if (!row || typeof row !== 'object') {
continue
}
const { context, name, role, text } = row as TranscriptRow
if (role === 'tool') {
pending.push(buildToolTrailLine(name ?? 'tool', context ?? ''))
continue
}
if (typeof text !== 'string' || !text.trim()) {
continue
}
if (role === 'assistant') {
out.push({ role, text, ...(pending.length && { tools: pending }) })
pending = []
} else if (role === 'user' || role === 'system') {
out.push({ role, text })
pending = []
}
}
return out
}
export const fmtDuration = (ms: number) => {
const t = Math.max(0, Math.floor(ms / 1000))
const h = Math.floor(t / 3600)
const m = Math.floor((t % 3600) / 60)
const s = t % 60
return h > 0 ? `${h}h ${m}m` : m > 0 ? `${m}m ${s}s` : `${s}s`
}
interface ImageMeta {
height?: number
token_estimate?: number
width?: number
}
interface TranscriptRow {
context?: string
name?: string
role?: string
text?: string
}
+16
View File
@@ -0,0 +1,16 @@
export const shortCwd = (cwd: string, max = 28) => {
const h = process.env.HOME
const p = h && cwd.startsWith(h) ? `~${cwd.slice(h.length)}` : cwd
return p.length <= max ? p : `${p.slice(-(max - 1))}`
}
export const fmtCwdBranch = (cwd: string, branch: null | string, max = 40) => {
if (!branch) {
return shortCwd(cwd, max)
}
const tag = ` (${branch.length > 16 ? `${branch.slice(-15)}` : branch})`
return `${shortCwd(cwd, Math.max(8, max - tag.length))}${tag}`
}
+11
View File
@@ -0,0 +1,11 @@
export const providerDisplayNames = (providers: readonly { name: string; slug: string }[]): string[] => {
const counts = new Map<string, number>()
for (const p of providers) {
counts.set(p.name, (counts.get(p.name) ?? 0) + 1)
}
return providers.map(p =>
(counts.get(p.name) ?? 0) > 1 && p.slug && p.slug !== p.name ? `${p.name} (${p.slug})` : p.name
)
}
+9
View File
@@ -0,0 +1,9 @@
import type { Theme } from '../theme.js'
import type { Role } from '../types.js'
export const ROLE: Record<Role, (t: Theme) => { body: string; glyph: string; prefix: string }> = {
assistant: t => ({ body: t.color.text, glyph: t.brand.tool, prefix: t.color.border }),
system: t => ({ body: '', glyph: '·', prefix: t.color.muted }),
tool: t => ({ body: t.color.muted, glyph: '⚡', prefix: t.color.muted }),
user: t => ({ body: t.color.label, glyph: t.brand.prompt, prefix: t.color.label })
}
+10
View File
@@ -0,0 +1,10 @@
/** Appended to `/model` args from the TUI picker for session scope; stripped in `session` slash before `config.set`. */
export const TUI_SESSION_MODEL_FLAG = '--tui-session'
export const looksLikeSlashCommand = (text: string) => /^\/[^\s/]*(?:\s|$)/.test(text)
export const parseSlashCommand = (cmd: string) => {
const [name = '', ...rest] = cmd.slice(1).split(/\s+/)
return { arg: rest.join(' '), cmd, name: name.toLowerCase() }
}
+3
View File
@@ -0,0 +1,3 @@
import type { Usage } from '../types.js'
export const ZERO: Usage = { calls: 0, input: 0, output: 0, total: 0 }
+51
View File
@@ -0,0 +1,51 @@
import type { Msg } from '../types.js'
import { userDisplay } from './messages.js'
const upperBound = (offsets: ArrayLike<number>, target: number) => {
let lo = 0
let hi = offsets.length
while (lo < hi) {
const mid = (lo + hi) >> 1
offsets[mid]! <= target ? (lo = mid + 1) : (hi = mid)
}
return lo
}
export const stickyPromptFromViewport = (
messages: readonly Msg[],
offsets: ArrayLike<number>,
top: number,
bottom: number,
sticky: boolean
) => {
if (sticky || !messages.length) {
return ''
}
const first = Math.max(0, upperBound(offsets, top) - 1)
const last = Math.max(first, upperBound(offsets, bottom) - 1)
const visibleStart = Math.min(messages.length, first)
const visibleEnd = Math.min(messages.length - 1, last)
for (let i = visibleStart; i <= visibleEnd; i++) {
if (messages[i]?.role === 'user') {
return ''
}
}
for (let i = Math.min(messages.length - 1, visibleStart - 1); i >= 0; i--) {
if (messages[i]?.role !== 'user') {
continue
}
return (offsets[i + 1] ?? (offsets[i] ?? 0) + 1) <= top
? userDisplay(messages[i]!.text.trim()).replace(/\s+/g, ' ').trim()
: ''
}
return ''
}