Files
hermes-agent/apps/desktop/src/app/session/hooks/use-route-resume.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

116 lines
3.4 KiB
TypeScript

import { type MutableRefObject, useEffect, useRef } from 'react'
import { isNewChatRoute } from '@/app/routes'
interface RouteResumeOptions {
activeSessionId: string | null
activeSessionIdRef: MutableRefObject<string | null>
creatingSessionRef: MutableRefObject<boolean>
currentView: string
freshDraftReady: boolean
gatewayState: string | undefined
locationPathname: string
resumeSession: (sessionId: string, focus: boolean) => Promise<unknown>
routedSessionId: string | null
runtimeIdByStoredSessionIdRef: MutableRefObject<Map<string, string>>
selectedStoredSessionId: string | null
selectedStoredSessionIdRef: MutableRefObject<string | null>
startFreshSessionDraft: (focus: boolean) => unknown
}
// HashRouter boot edge case: pathname briefly reads `/` before the hash is
// parsed. If the hash references a real session, defer; resume picks it up
// next tick. Without this, ctrl+R on `#/:sessionId` flashes 5 loading states.
function rawHashLooksLikeSession(): boolean {
if (typeof window === 'undefined') {
return false
}
const hash = window.location.hash.replace(/^#/, '')
if (!hash || hash === '/') {
return false
}
return (
!hash.startsWith('/settings') &&
!hash.startsWith('/skills') &&
!hash.startsWith('/messaging') &&
!hash.startsWith('/artifacts')
)
}
export function useRouteResume({
activeSessionId,
activeSessionIdRef,
creatingSessionRef,
currentView,
freshDraftReady,
gatewayState,
locationPathname,
resumeSession,
routedSessionId,
runtimeIdByStoredSessionIdRef,
selectedStoredSessionId,
selectedStoredSessionIdRef,
startFreshSessionDraft
}: RouteResumeOptions) {
const lastPathnameRef = useRef<string | null>(null)
const wasGatewayOpenRef = useRef(false)
useEffect(() => {
const gatewayOpen = gatewayState === 'open'
const pathnameChanged = lastPathnameRef.current !== locationPathname
const gatewayBecameOpen = !wasGatewayOpenRef.current && gatewayOpen
lastPathnameRef.current = locationPathname
wasGatewayOpenRef.current = gatewayOpen
if (currentView !== 'chat' || !gatewayOpen) {
return
}
if (routedSessionId) {
const cachedRuntime = runtimeIdByStoredSessionIdRef.current.get(routedSessionId)
const alreadyActive =
routedSessionId === selectedStoredSessionIdRef.current &&
Boolean(cachedRuntime) &&
cachedRuntime === activeSessionIdRef.current
// Resume only when the route meaningfully changed (or gateway just opened).
// This avoids a transient /:sid re-resume during "new chat" state clears
// before the pathname updates from /:sid -> /.
const shouldResume = pathnameChanged || gatewayBecameOpen
if (!alreadyActive && shouldResume && !creatingSessionRef.current) {
void resumeSession(routedSessionId, true)
}
return
}
if (
isNewChatRoute(locationPathname) &&
!creatingSessionRef.current &&
(selectedStoredSessionId || activeSessionId || !freshDraftReady) &&
!rawHashLooksLikeSession()
) {
startFreshSessionDraft(true)
}
}, [
activeSessionId,
activeSessionIdRef,
creatingSessionRef,
currentView,
freshDraftReady,
gatewayState,
locationPathname,
resumeSession,
routedSessionId,
runtimeIdByStoredSessionIdRef,
selectedStoredSessionId,
selectedStoredSessionIdRef,
startFreshSessionDraft
])
}