mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-01 01:51:44 +00:00
chore: uptick
This commit is contained in:
parent
82197a87dc
commit
eda400d8a5
5 changed files with 176 additions and 94 deletions
|
|
@ -868,7 +868,12 @@ def _on_tool_progress(
|
||||||
if _kwargs.get("toolsets"):
|
if _kwargs.get("toolsets"):
|
||||||
payload["toolsets"] = [str(t) for t in _kwargs["toolsets"]]
|
payload["toolsets"] = [str(t) for t in _kwargs["toolsets"]]
|
||||||
# Per-branch rollups emitted on subagent.complete (features 1+2+4).
|
# Per-branch rollups emitted on subagent.complete (features 1+2+4).
|
||||||
for int_key in ("input_tokens", "output_tokens", "reasoning_tokens", "api_calls"):
|
for int_key in (
|
||||||
|
"input_tokens",
|
||||||
|
"output_tokens",
|
||||||
|
"reasoning_tokens",
|
||||||
|
"api_calls",
|
||||||
|
):
|
||||||
val = _kwargs.get(int_key)
|
val = _kwargs.get(int_key)
|
||||||
if val is not None:
|
if val is not None:
|
||||||
try:
|
try:
|
||||||
|
|
@ -1738,16 +1743,20 @@ def _(rid, params: dict) -> dict:
|
||||||
# Layout: $HERMES_HOME/spawn-trees/<session_id>/<timestamp>.json
|
# Layout: $HERMES_HOME/spawn-trees/<session_id>/<timestamp>.json
|
||||||
# Each file contains { session_id, started_at, finished_at, subagents: [...] }.
|
# Each file contains { session_id, started_at, finished_at, subagents: [...] }.
|
||||||
|
|
||||||
|
|
||||||
def _spawn_trees_root():
|
def _spawn_trees_root():
|
||||||
from pathlib import Path as _P
|
from pathlib import Path as _P
|
||||||
from hermes_constants import get_hermes_home
|
from hermes_constants import get_hermes_home
|
||||||
|
|
||||||
root = get_hermes_home() / "spawn-trees"
|
root = get_hermes_home() / "spawn-trees"
|
||||||
root.mkdir(parents=True, exist_ok=True)
|
root.mkdir(parents=True, exist_ok=True)
|
||||||
return root
|
return root
|
||||||
|
|
||||||
|
|
||||||
def _spawn_tree_session_dir(session_id: str):
|
def _spawn_tree_session_dir(session_id: str):
|
||||||
safe = "".join(c if c.isalnum() or c in "-_" else "_" for c in session_id) or "unknown"
|
safe = (
|
||||||
|
"".join(c if c.isalnum() or c in "-_" else "_" for c in session_id) or "unknown"
|
||||||
|
)
|
||||||
d = _spawn_trees_root() / safe
|
d = _spawn_trees_root() / safe
|
||||||
d.mkdir(parents=True, exist_ok=True)
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
return d
|
return d
|
||||||
|
|
@ -1797,6 +1806,7 @@ def _(rid, params: dict) -> dict:
|
||||||
return _err(rid, 4000, "subagents list required")
|
return _err(rid, 4000, "subagents list required")
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
started_at = params.get("started_at")
|
started_at = params.get("started_at")
|
||||||
finished_at = params.get("finished_at") or time.time()
|
finished_at = params.get("finished_at") or time.time()
|
||||||
label = str(params.get("label") or "")
|
label = str(params.get("label") or "")
|
||||||
|
|
@ -1816,14 +1826,17 @@ def _(rid, params: dict) -> dict:
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
return _err(rid, 5000, f"spawn_tree.save failed: {exc}")
|
return _err(rid, 5000, f"spawn_tree.save failed: {exc}")
|
||||||
|
|
||||||
_append_spawn_tree_index(d, {
|
_append_spawn_tree_index(
|
||||||
"path": str(path),
|
d,
|
||||||
"session_id": session_id,
|
{
|
||||||
"started_at": payload["started_at"],
|
"path": str(path),
|
||||||
"finished_at": payload["finished_at"],
|
"session_id": session_id,
|
||||||
"label": label,
|
"started_at": payload["started_at"],
|
||||||
"count": len(subagents),
|
"finished_at": payload["finished_at"],
|
||||||
})
|
"label": label,
|
||||||
|
"count": len(subagents),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return _ok(rid, {"path": str(path), "session_id": session_id})
|
return _ok(rid, {"path": str(path), "session_id": session_id})
|
||||||
|
|
||||||
|
|
@ -1845,7 +1858,9 @@ def _(rid, params: dict) -> dict:
|
||||||
indexed = _read_spawn_tree_index(d)
|
indexed = _read_spawn_tree_index(d)
|
||||||
if indexed:
|
if indexed:
|
||||||
# Skip index entries whose snapshot file was manually deleted.
|
# Skip index entries whose snapshot file was manually deleted.
|
||||||
entries.extend(e for e in indexed if (p := e.get("path")) and Path(p).exists())
|
entries.extend(
|
||||||
|
e for e in indexed if (p := e.get("path")) and Path(p).exists()
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Fallback for legacy (pre-index) sessions: full scan. O(N) reads
|
# Fallback for legacy (pre-index) sessions: full scan. O(N) reads
|
||||||
|
|
@ -1860,14 +1875,16 @@ def _(rid, params: dict) -> dict:
|
||||||
except Exception:
|
except Exception:
|
||||||
raw = {}
|
raw = {}
|
||||||
subagents = raw.get("subagents") or []
|
subagents = raw.get("subagents") or []
|
||||||
entries.append({
|
entries.append(
|
||||||
"path": str(p),
|
{
|
||||||
"session_id": raw.get("session_id") or d.name,
|
"path": str(p),
|
||||||
"finished_at": raw.get("finished_at") or stat.st_mtime,
|
"session_id": raw.get("session_id") or d.name,
|
||||||
"started_at": raw.get("started_at"),
|
"finished_at": raw.get("finished_at") or stat.st_mtime,
|
||||||
"label": raw.get("label") or "",
|
"started_at": raw.get("started_at"),
|
||||||
"count": len(subagents) if isinstance(subagents, list) else 0,
|
"label": raw.get("label") or "",
|
||||||
})
|
"count": len(subagents) if isinstance(subagents, list) else 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
except OSError:
|
except OSError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
@ -1878,6 +1895,7 @@ def _(rid, params: dict) -> dict:
|
||||||
@method("spawn_tree.load")
|
@method("spawn_tree.load")
|
||||||
def _(rid, params: dict) -> dict:
|
def _(rid, params: dict) -> dict:
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
raw_path = str(params.get("path") or "").strip()
|
raw_path = str(params.get("path") or "").strip()
|
||||||
if not raw_path:
|
if not raw_path:
|
||||||
return _err(rid, 4000, "path required")
|
return _err(rid, 4000, "path required")
|
||||||
|
|
|
||||||
|
|
@ -141,8 +141,7 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
||||||
// Terminal statuses are never overwritten by late-arriving live events —
|
// Terminal statuses are never overwritten by late-arriving live events —
|
||||||
// otherwise a stale `subagent.start` / `spawn_requested` can clobber a
|
// otherwise a stale `subagent.start` / `spawn_requested` can clobber a
|
||||||
// `failed` or `interrupted` terminal state (Copilot review #14045).
|
// `failed` or `interrupted` terminal state (Copilot review #14045).
|
||||||
const isTerminalStatus = (s: SubagentProgress['status']) =>
|
const isTerminalStatus = (s: SubagentProgress['status']) => s === 'completed' || s === 'failed' || s === 'interrupted'
|
||||||
s === 'completed' || s === 'failed' || s === 'interrupted'
|
|
||||||
|
|
||||||
const keepTerminalElseRunning = (s: SubagentProgress['status']) => (isTerminalStatus(s) ? s : 'running')
|
const keepTerminalElseRunning = (s: SubagentProgress['status']) => (isTerminalStatus(s) ? s : 'running')
|
||||||
|
|
||||||
|
|
@ -410,10 +409,16 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
turnController.upsertSubagent(ev.payload, c => ({
|
// Update-only: never resurrect subagents whose spawn_requested/start
|
||||||
status: keepTerminalElseRunning(c.status),
|
// we missed or that already flushed via message.complete.
|
||||||
thinking: pushThinking(c.thinking, text)
|
turnController.upsertSubagent(
|
||||||
}))
|
ev.payload,
|
||||||
|
c => ({
|
||||||
|
status: keepTerminalElseRunning(c.status),
|
||||||
|
thinking: pushThinking(c.thinking, text)
|
||||||
|
}),
|
||||||
|
{ createIfMissing: false }
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -424,10 +429,14 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
||||||
ev.payload.tool_preview ?? ev.payload.text ?? ''
|
ev.payload.tool_preview ?? ev.payload.text ?? ''
|
||||||
)
|
)
|
||||||
|
|
||||||
turnController.upsertSubagent(ev.payload, c => ({
|
turnController.upsertSubagent(
|
||||||
status: keepTerminalElseRunning(c.status),
|
ev.payload,
|
||||||
tools: pushTool(c.tools, line)
|
c => ({
|
||||||
}))
|
status: keepTerminalElseRunning(c.status),
|
||||||
|
tools: pushTool(c.tools, line)
|
||||||
|
}),
|
||||||
|
{ createIfMissing: false }
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -439,20 +448,28 @@ export function createGatewayEventHandler(ctx: GatewayEventHandlerContext): (ev:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
turnController.upsertSubagent(ev.payload, c => ({
|
turnController.upsertSubagent(
|
||||||
notes: pushNote(c.notes, text),
|
ev.payload,
|
||||||
status: keepTerminalElseRunning(c.status)
|
c => ({
|
||||||
}))
|
notes: pushNote(c.notes, text),
|
||||||
|
status: keepTerminalElseRunning(c.status)
|
||||||
|
}),
|
||||||
|
{ createIfMissing: false }
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
case 'subagent.complete':
|
case 'subagent.complete':
|
||||||
turnController.upsertSubagent(ev.payload, c => ({
|
turnController.upsertSubagent(
|
||||||
durationSeconds: ev.payload.duration_seconds ?? c.durationSeconds,
|
ev.payload,
|
||||||
status: ev.payload.status ?? 'completed',
|
c => ({
|
||||||
summary: ev.payload.summary || ev.payload.text || c.summary
|
durationSeconds: ev.payload.duration_seconds ?? c.durationSeconds,
|
||||||
}))
|
status: ev.payload.status ?? 'completed',
|
||||||
|
summary: ev.payload.summary || ev.payload.text || c.summary
|
||||||
|
}),
|
||||||
|
{ createIfMissing: false }
|
||||||
|
)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -460,7 +460,11 @@ class TurnController {
|
||||||
patchTurnState({ activity: [], outcome: '', subagents: [], toolTokens: 0, tools: [], turnTrail: [] })
|
patchTurnState({ activity: [], outcome: '', subagents: [], toolTokens: 0, tools: [], turnTrail: [] })
|
||||||
}
|
}
|
||||||
|
|
||||||
upsertSubagent(p: SubagentEventPayload, patch: (current: SubagentProgress) => Partial<SubagentProgress>) {
|
upsertSubagent(
|
||||||
|
p: SubagentEventPayload,
|
||||||
|
patch: (current: SubagentProgress) => Partial<SubagentProgress>,
|
||||||
|
opts: { createIfMissing?: boolean } = { createIfMissing: true }
|
||||||
|
) {
|
||||||
// Stable id: prefer the server-issued subagent_id (survives nested
|
// Stable id: prefer the server-issued subagent_id (survives nested
|
||||||
// grandchildren + cross-tree joins). Fall back to the composite key
|
// grandchildren + cross-tree joins). Fall back to the composite key
|
||||||
// for older gateways that omit the field — those produce a flat list.
|
// for older gateways that omit the field — those produce a flat list.
|
||||||
|
|
@ -469,6 +473,14 @@ class TurnController {
|
||||||
patchTurnState(state => {
|
patchTurnState(state => {
|
||||||
const existing = state.subagents.find(item => item.id === id)
|
const existing = state.subagents.find(item => item.id === id)
|
||||||
|
|
||||||
|
// Late events (subagent.complete/tool/progress arriving after message.complete
|
||||||
|
// has already fired idle()) would otherwise resurrect a finished
|
||||||
|
// subagent into turn.subagents and block the "finished" title on the
|
||||||
|
// /agents overlay. When `createIfMissing` is false we drop silently.
|
||||||
|
if (!existing && !opts.createIfMissing) {
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
const base: SubagentProgress = existing ?? {
|
const base: SubagentProgress = existing ?? {
|
||||||
depth: p.depth ?? 0,
|
depth: p.depth ?? 0,
|
||||||
goal: p.goal,
|
goal: p.goal,
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,35 @@ const fmtDur = (seconds?: number): string => {
|
||||||
return s === 0 ? `${m}m` : `${m}m ${s}s`
|
return s === 0 ? `${m}m` : `${m}m ${s}s`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Server duration if present; else live edge from `startedAt` (running / queued). */
|
||||||
|
const displayElapsedSeconds = (item: SubagentProgress, nowMs: number): number | null => {
|
||||||
|
if (item.durationSeconds != null) {
|
||||||
|
return item.durationSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.startedAt != null && (item.status === 'running' || item.status === 'queued')) {
|
||||||
|
return Math.max(0, (nowMs - item.startedAt) / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Like fmtDur but allows 0s for just-started / still-running rows. */
|
||||||
|
const fmtElapsedLabel = (seconds: number): string => {
|
||||||
|
if (seconds < 0) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seconds < 60) {
|
||||||
|
return `${Math.max(0, Math.round(seconds))}s`
|
||||||
|
}
|
||||||
|
|
||||||
|
const m = Math.floor(seconds / 60)
|
||||||
|
const s = Math.round(seconds - m * 60)
|
||||||
|
|
||||||
|
return s === 0 ? `${m}m` : `${m}m ${s}s`
|
||||||
|
}
|
||||||
|
|
||||||
const indentFor = (depth: number): string => ' '.repeat(Math.max(0, depth))
|
const indentFor = (depth: number): string => ' '.repeat(Math.max(0, depth))
|
||||||
const formatRowId = (n: number): string => String(n + 1).padStart(2, ' ')
|
const formatRowId = (n: number): string => String(n + 1).padStart(2, ' ')
|
||||||
const cycle = <T,>(order: readonly T[], current: T): T => order[(order.indexOf(current) + 1) % order.length]!
|
const cycle = <T,>(order: readonly T[], current: T): T => order[(order.indexOf(current) + 1) % order.length]!
|
||||||
|
|
@ -229,8 +258,11 @@ function GanttStrip({
|
||||||
const totalSeconds = (globalEnd - globalStart) / 1000
|
const totalSeconds = (globalEnd - globalStart) / 1000
|
||||||
|
|
||||||
// 5-col id gutter (" 12 ") so the bar doesn't press against the id.
|
// 5-col id gutter (" 12 ") so the bar doesn't press against the id.
|
||||||
|
// 10-col right reserve: pad + up to `12m 30s`-style label without
|
||||||
|
// truncate-end against a full-width bar.
|
||||||
const idGutter = 5
|
const idGutter = 5
|
||||||
const barWidth = Math.max(10, cols - idGutter - 2)
|
const labelReserve = 10
|
||||||
|
const barWidth = Math.max(10, cols - idGutter - labelReserve)
|
||||||
const startIdx = Math.max(0, Math.min(Math.max(0, spans.length - maxRows), cursor - Math.floor(maxRows / 2)))
|
const startIdx = Math.max(0, Math.min(Math.max(0, spans.length - maxRows), cursor - Math.floor(maxRows / 2)))
|
||||||
const shown = spans.slice(startIdx, startIdx + maxRows)
|
const shown = spans.slice(startIdx, startIdx + maxRows)
|
||||||
|
|
||||||
|
|
@ -242,15 +274,26 @@ function GanttStrip({
|
||||||
return ' '.repeat(s) + '█'.repeat(fill) + ' '.repeat(Math.max(0, barWidth - s - fill))
|
return ' '.repeat(s) + '█'.repeat(fill) + ' '.repeat(Math.max(0, barWidth - s - fill))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Tick ruler + second labels. Fixed-length char array guarantees
|
// Wall-clock axis: more ticks on short windows so the scale visibly
|
||||||
// `.length === barWidth` (an earlier padEnd+skip loop wrapped to a
|
// “counts up” with `now` instead of a single 0/10s pair.
|
||||||
// second row which looked like garbled duplicated labels).
|
const charStep = totalSeconds < 20 && barWidth > 20 ? 5 : 10
|
||||||
const ruler = Array.from({ length: barWidth }, (_, i) => (i > 0 && i % 10 === 0 ? '┼' : '─')).join('')
|
|
||||||
|
const ruler = Array.from({ length: barWidth }, (_, i) => {
|
||||||
|
if (i > 0 && i % 10 === 0) {
|
||||||
|
return '┼'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i > 0 && i % 5 === 0) {
|
||||||
|
return '·'
|
||||||
|
}
|
||||||
|
|
||||||
|
return '─'
|
||||||
|
}).join('')
|
||||||
|
|
||||||
const rulerLabels = (() => {
|
const rulerLabels = (() => {
|
||||||
const chars = new Array(barWidth).fill(' ')
|
const chars = new Array(barWidth).fill(' ')
|
||||||
|
|
||||||
for (let pos = 0; pos < barWidth; pos += 10) {
|
for (let pos = 0; pos < barWidth; pos += charStep) {
|
||||||
const secs = (pos / barWidth) * totalSeconds
|
const secs = (pos / barWidth) * totalSeconds
|
||||||
const label = pos === 0 ? '0' : secs >= 1 ? `${Math.round(secs)}s` : `${secs.toFixed(1)}s`
|
const label = pos === 0 ? '0' : secs >= 1 ? `${Math.round(secs)}s` : `${secs.toFixed(1)}s`
|
||||||
|
|
||||||
|
|
@ -268,7 +311,7 @@ function GanttStrip({
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" marginBottom={1}>
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
<Text color={t.color.dim}>
|
<Text color={t.color.dim}>
|
||||||
Timeline · {fmtDur(totalSeconds)}
|
Timeline · {fmtElapsedLabel(Math.max(0, totalSeconds))}
|
||||||
{windowLabel}
|
{windowLabel}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
|
@ -277,21 +320,24 @@ function GanttStrip({
|
||||||
const { color } = statusGlyph(node.item, t)
|
const { color } = statusGlyph(node.item, t)
|
||||||
const accent = active ? t.color.amber : t.color.dim
|
const accent = active ? t.color.amber : t.color.dim
|
||||||
|
|
||||||
const durLabel = node.item.durationSeconds
|
const elSec = displayElapsedSeconds(node.item, now)
|
||||||
? fmtDur(node.item.durationSeconds)
|
const elLabel = elSec != null ? fmtElapsedLabel(elSec) : ''
|
||||||
: node.item.status === 'running'
|
|
||||||
? 'running'
|
|
||||||
: ''
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text key={node.item.id} wrap="truncate-end">
|
<Text key={node.item.id} wrap="truncate-end">
|
||||||
<Text bold={active} color={accent}>
|
<Text bold={active} color={accent}>
|
||||||
{formatRowId(idx)}{' '}
|
{formatRowId(idx)}
|
||||||
|
{' '}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text color={active ? t.color.amber : color}>{bar(startAt, endAt)}</Text>
|
<Text color={active ? t.color.amber : color}>{bar(startAt, endAt)}</Text>
|
||||||
|
|
||||||
{durLabel ? <Text color={accent}> {durLabel}</Text> : null}
|
{elLabel ? (
|
||||||
|
<Text color={accent}>
|
||||||
|
{' '}
|
||||||
|
{elLabel}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
@ -301,7 +347,7 @@ function GanttStrip({
|
||||||
{ruler}
|
{ruler}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{totalSeconds >= 2 ? (
|
{totalSeconds > 0 ? (
|
||||||
<Text color={t.color.dim} dim>
|
<Text color={t.color.dim} dim>
|
||||||
{' '}
|
{' '}
|
||||||
{rulerLabels}
|
{rulerLabels}
|
||||||
|
|
@ -504,6 +550,7 @@ function ListRow({
|
||||||
active,
|
active,
|
||||||
index,
|
index,
|
||||||
node,
|
node,
|
||||||
|
now,
|
||||||
peak,
|
peak,
|
||||||
t,
|
t,
|
||||||
width
|
width
|
||||||
|
|
@ -511,6 +558,7 @@ function ListRow({
|
||||||
active: boolean
|
active: boolean
|
||||||
index: number
|
index: number
|
||||||
node: SubagentNode
|
node: SubagentNode
|
||||||
|
now: number
|
||||||
peak: number
|
peak: number
|
||||||
t: Theme
|
t: Theme
|
||||||
width: number
|
width: number
|
||||||
|
|
@ -523,35 +571,24 @@ function ListRow({
|
||||||
const goal = compactPreview(node.item.goal || 'subagent', width - 24 - node.item.depth * 2)
|
const goal = compactPreview(node.item.goal || 'subagent', width - 24 - node.item.depth * 2)
|
||||||
const tools = node.aggregate.totalTools > 0 ? ` ·${node.aggregate.totalTools}t` : ''
|
const tools = node.aggregate.totalTools > 0 ? ` ·${node.aggregate.totalTools}t` : ''
|
||||||
const kids = node.children.length ? ` ·${node.children.length}↓` : ''
|
const kids = node.children.length ? ` ·${node.children.length}↓` : ''
|
||||||
const dur = fmtDur(node.item.durationSeconds)
|
const elSec = displayElapsedSeconds(node.item, now)
|
||||||
|
const elapsed = elSec != null ? fmtElapsedLabel(elSec) : ''
|
||||||
|
|
||||||
// Selection pattern mirrors sessionPicker: inverse + amber for contrast
|
// Selection pattern mirrors sessionPicker: inverse + amber for contrast
|
||||||
// across any theme, body stays cornsilk, stats dim.
|
// across any theme, body stays cornsilk, stats dim.
|
||||||
const fg = active ? t.color.amber : t.color.cornsilk
|
const fg = active ? t.color.amber : t.color.cornsilk
|
||||||
|
|
||||||
// Heat marker + glyph occupy a fixed 3-char gutter so the goal text
|
|
||||||
// aligns across hot and cool rows. One space on each side of the glyph
|
|
||||||
// gives the status dot breathing room — otherwise it reads glued to the
|
|
||||||
// heat bar and the goal text.
|
|
||||||
const prefix = heatMarker ? (
|
|
||||||
<Text color={heatMarker}>▍ </Text>
|
|
||||||
) : (
|
|
||||||
<Text>{' '}</Text>
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Text bold={active} color={fg} inverse={active} wrap="truncate-end">
|
<Text bold={active} color={fg} inverse={active} wrap="truncate-end">
|
||||||
{active ? '▸ ' : ' '}
|
{' '}
|
||||||
<Text color={active ? fg : t.color.dim}>{formatRowId(index)} </Text>
|
<Text color={active ? fg : t.color.dim}>{formatRowId(index)} </Text>
|
||||||
{indentFor(node.item.depth)}
|
{indentFor(node.item.depth)}
|
||||||
{prefix}
|
{heatMarker ? <Text color={heatMarker}>▍</Text> : null}
|
||||||
<Text color={active ? fg : color}>{glyph}</Text>
|
<Text color={active ? fg : color}>{glyph}</Text> {goal}
|
||||||
{' '}
|
|
||||||
{goal}
|
|
||||||
<Text color={active ? fg : t.color.dim}>
|
<Text color={active ? fg : t.color.dim}>
|
||||||
{tools}
|
{tools}
|
||||||
{kids}
|
{kids}
|
||||||
{dur ? ` · ${dur}` : ''}
|
{elapsed ? ` · ${elapsed}` : ''}
|
||||||
</Text>
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
)
|
)
|
||||||
|
|
@ -943,20 +980,23 @@ export function AgentsOverlay({ gw, initialHistoryIndex = 0, onClose, t }: Agent
|
||||||
? `caps d${delegation.maxSpawnDepth}/${delegation.maxConcurrentChildren ?? '?'}`
|
? `caps d${delegation.maxSpawnDepth}/${delegation.maxConcurrentChildren ?? '?'}`
|
||||||
: ''
|
: ''
|
||||||
|
|
||||||
// One-line title. An earlier version had a separate "subtitle" with the
|
// Single header line — title · metrics. An earlier "title + subtitle"
|
||||||
// full snapshot label; narrow terminals wrapped it instead of truncating,
|
// variant wrapped on narrow terminals which looked like the header was
|
||||||
// which looked like the header was double-rendered.
|
// rendering twice, and a one-line header makes it obvious at a glance
|
||||||
|
// whether the turn is live or finished.
|
||||||
const title = (() => {
|
const title = (() => {
|
||||||
if (!replayMode || !effectiveSnapshot) {
|
if (!replayMode || !effectiveSnapshot) {
|
||||||
return `Spawn tree${delegation.paused ? ' · ⏸ paused' : ''}`
|
return `Spawn tree${delegation.paused ? ' · ⏸ paused' : ''}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const at = new Date(effectiveSnapshot.finishedAt).toLocaleTimeString()
|
const at = new Date(effectiveSnapshot.finishedAt).toLocaleTimeString()
|
||||||
const position = historyIndex > 0 ? `Replay · ${historyIndex}/${history.length}` : 'Last turn'
|
const position = historyIndex > 0 ? `Replay ${historyIndex}/${history.length}` : 'Last turn'
|
||||||
|
|
||||||
return `${position} · finished ${at}`
|
return `${position} · finished ${at}`
|
||||||
})()
|
})()
|
||||||
|
|
||||||
|
const metaLine = [formatSummary(totals), spark, capsLabel, mix ? `· ${mix}` : ''].filter(Boolean).join(' ')
|
||||||
|
|
||||||
const controlsHint = replayMode
|
const controlsHint = replayMode
|
||||||
? ' · controls locked'
|
? ' · controls locked'
|
||||||
: ` · x kill · X subtree · p ${delegation.paused ? 'resume' : 'pause'}`
|
: ` · x kill · X subtree · p ${delegation.paused ? 'resume' : 'pause'}`
|
||||||
|
|
@ -970,15 +1010,16 @@ export function AgentsOverlay({ gw, initialHistoryIndex = 0, onClose, t }: Agent
|
||||||
return (
|
return (
|
||||||
<Box alignItems="stretch" flexDirection="column" flexGrow={1} paddingX={1} paddingY={1}>
|
<Box alignItems="stretch" flexDirection="column" flexGrow={1} paddingX={1} paddingY={1}>
|
||||||
<Box flexDirection="column" marginBottom={1}>
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
<Text bold color={replayMode ? t.color.bronze : t.color.gold} wrap="truncate-end">
|
<Text wrap="truncate-end">
|
||||||
{title}
|
<Text bold color={replayMode ? t.color.bronze : t.color.gold}>
|
||||||
</Text>
|
{title}
|
||||||
|
</Text>
|
||||||
<Text color={t.color.dim} wrap="truncate-end">
|
{metaLine ? (
|
||||||
{formatSummary(totals)}
|
<Text color={t.color.dim}>
|
||||||
{spark ? ` ${spark}` : ''}
|
{' '}
|
||||||
{capsLabel ? ` ${capsLabel}` : ''}
|
{metaLine}
|
||||||
{mix ? ` · ${mix}` : ''}
|
</Text>
|
||||||
|
) : null}
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
@ -997,6 +1038,7 @@ export function AgentsOverlay({ gw, initialHistoryIndex = 0, onClose, t }: Agent
|
||||||
index={listWindowStart + i}
|
index={listWindowStart + i}
|
||||||
key={node.item.id}
|
key={node.item.id}
|
||||||
node={node}
|
node={node}
|
||||||
|
now={now}
|
||||||
peak={peak}
|
peak={peak}
|
||||||
t={t}
|
t={t}
|
||||||
width={cols}
|
width={cols}
|
||||||
|
|
|
||||||
|
|
@ -615,14 +615,7 @@ export function TextInput({
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if ((k.ctrl && inp === 'c') || k.tab || (k.shift && k.tab) || k.pageUp || k.pageDown || k.escape) {
|
||||||
(k.ctrl && inp === 'c') ||
|
|
||||||
k.tab ||
|
|
||||||
(k.shift && k.tab) ||
|
|
||||||
k.pageUp ||
|
|
||||||
k.pageDown ||
|
|
||||||
k.escape
|
|
||||||
) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue