Files
hermes-agent/apps/desktop/src/components/chat/activity-timer.ts
T
Brooklyn Nicholson d208f2c2c0 feat(desktop): reconcile live tool events, polish thread chrome, harden boot
- chat-messages: match tool rows by overlapping query/context/preview values
  so preview-first `tool.progress` rows reliably adopt later stable-id
  `tool.start` payloads instead of spawning ghost rows or mis-merging
  parallel same-name calls; preserve prior args/result across phases.
- tui_gateway: emit full args + parsed result on `tool.start` / `tool.complete`,
  drop redundant `tool.started` re-emit from `tool.progress`.
- electron/main: prefer SOURCE_REPO_ROOT before PATH `hermes` in dev so
  local backend edits actually run; split hardening helpers into
  `electron/hardening.cjs` with tests.
- thread/tool UI: one-shot enter animation keyed by stable ids, braille
  spinner for running rows, Cursor-like disclosure rows, drill-down +
  duration/count formatting via new tool-fallback-model.
- composer: extract `text-utils`, drop liquid-glass overrides.
- right-rail: split preview-pane into preview-console / preview-file.
- runtime: incremental external-store runtime + runtime-readiness gate;
  onboarding store + tests; route-resume hook test.
- regression tests for live tool reconciliation (parallel tools, id-less
  progress, preview-first rows, structured args/results).
2026-05-11 21:38:47 -04:00

63 lines
1.6 KiB
TypeScript

import { useEffect, useRef, useState } from 'react'
// Module-level registry so timers survive component unmount/remount (e.g.
// when a tool row scrolls out and back). Keyed by caller-supplied timerKey;
// anonymous timers (no key) start fresh each mount.
const startedAtByKey = new Map<string, number>()
function startedAt(key?: string): number {
if (!key) {
return Date.now()
}
const existing = startedAtByKey.get(key)
if (existing !== undefined) {
return existing
}
const now = Date.now()
startedAtByKey.set(key, now)
return now
}
export function formatElapsed(seconds: number): string {
if (seconds < 60) {
return `${seconds}s`
}
return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`
}
export function useElapsedSeconds(active = true, timerKey?: string): number {
const start = useRef(startedAt(timerKey))
const lastKey = useRef(timerKey)
const [elapsed, setElapsed] = useState(() => Math.max(0, Math.floor((Date.now() - start.current) / 1000)))
if (lastKey.current !== timerKey) {
start.current = startedAt(timerKey)
lastKey.current = timerKey
}
useEffect(() => {
if (!active) {
return
}
if (timerKey) {
start.current = startedAt(timerKey)
}
const tick = () => setElapsed(Math.max(0, Math.floor((Date.now() - start.current) / 1000)))
tick()
const id = window.setInterval(tick, 1000)
return () => window.clearInterval(id)
}, [active, timerKey])
return elapsed
}
export function __resetElapsedTimerRegistryForTests() {
startedAtByKey.clear()
}