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
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:
@@ -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]!
|
||||
@@ -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
|
||||
}
|
||||
@@ -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}`
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { Usage } from '../types.js'
|
||||
|
||||
export const ZERO: Usage = { calls: 0, input: 0, output: 0, total: 0 }
|
||||
@@ -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 ''
|
||||
}
|
||||
Reference in New Issue
Block a user