- 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).
116 lines
3.4 KiB
TypeScript
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
|
|
])
|
|
}
|