mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
Assistant text goes through the markdown renderer, not DirectiveContent, so a session reference an agent wrote came out as literal text. Rewrite bare refs into `#session/<value>` links during markdown preprocessing and dispatch that href to the shared chip in MarkdownLink, alongside the existing media and preview hrefs. Preprocessing already skips code fences and inline code, so a ref being discussed in code stays literal. The pure parsing/href helpers move to session-refs.ts to keep the resolver's React and API imports out of the per-flush preprocess path.
143 lines
3.9 KiB
TypeScript
143 lines
3.9 KiB
TypeScript
/**
|
|
* Resolves `@session:<profile>/<id>` reference values to the session's title.
|
|
*
|
|
* Same shape as the external-link title resolver (`external-link.tsx`): a
|
|
* process-lifetime cache, in-flight dedupe, and subscribers so every chip for
|
|
* the same session repaints off one lookup. The sidebar list answers most
|
|
* lookups for free; only an unknown id costs a REST round-trip.
|
|
*/
|
|
import { useEffect, useMemo, useState } from 'react'
|
|
|
|
import { getSession } from '@/hermes'
|
|
import { parseSessionRefValue, sessionRefCacheKey, sessionRefFallbackLabel } from '@/lib/session-refs'
|
|
import { $sessions, sessionMatchesStoredId } from '@/store/session'
|
|
import type { SessionInfo } from '@/types/hermes'
|
|
|
|
const titleCache = new Map<string, string>()
|
|
const titleInflight = new Map<string, Promise<string>>()
|
|
const titleSubs = new Map<string, Set<(value: string) => void>>()
|
|
|
|
/** Deliberately not `sessionTitle()` from chat-runtime: its "Untitled session"
|
|
* fallback is a worse chip label than the short id, so an untitled row
|
|
* resolves to empty and the caller's fallback wins. */
|
|
function sessionRowTitle(row: SessionInfo): string {
|
|
return row.title?.trim() || row.preview?.trim() || ''
|
|
}
|
|
|
|
function profileMatches(sessionProfile: null | string | undefined, target?: string): boolean {
|
|
if (!target) {
|
|
return true
|
|
}
|
|
|
|
return ((sessionProfile ?? '').trim() || 'default') === (target.trim() || 'default')
|
|
}
|
|
|
|
export function lookupLocalSessionTitle(value: string): string {
|
|
const { profile, sessionId } = parseSessionRefValue(value)
|
|
|
|
if (!sessionId) {
|
|
return ''
|
|
}
|
|
|
|
const row = $sessions
|
|
.get()
|
|
.find(session => sessionMatchesStoredId(session, sessionId) && profileMatches(session.profile, profile))
|
|
|
|
return row ? sessionRowTitle(row) : ''
|
|
}
|
|
|
|
/** REST lookup that can't throw: the bridge is absent outside Electron, and a
|
|
* session id that isn't on this backend 404s. Both mean "no title". */
|
|
function requestSessionRow(sessionId: string, profile?: string): Promise<null | SessionInfo> {
|
|
try {
|
|
return Promise.resolve(getSession(sessionId, profile ?? null)).catch(() => null)
|
|
} catch {
|
|
return Promise.resolve(null)
|
|
}
|
|
}
|
|
|
|
export function fetchSessionLinkTitle(value: string): Promise<string> {
|
|
const key = sessionRefCacheKey(value)
|
|
|
|
if (!key) {
|
|
return Promise.resolve('')
|
|
}
|
|
|
|
const cached = titleCache.get(key)
|
|
|
|
if (cached !== undefined) {
|
|
return Promise.resolve(cached)
|
|
}
|
|
|
|
const inflight = titleInflight.get(key)
|
|
|
|
if (inflight) {
|
|
return inflight
|
|
}
|
|
|
|
const local = lookupLocalSessionTitle(value)
|
|
|
|
if (local) {
|
|
titleCache.set(key, local)
|
|
|
|
return Promise.resolve(local)
|
|
}
|
|
|
|
const { profile, sessionId } = parseSessionRefValue(value)
|
|
|
|
const promise = requestSessionRow(sessionId, profile)
|
|
.then(row => (row ? sessionRowTitle(row) : ''))
|
|
.then(title => {
|
|
titleCache.set(key, title)
|
|
titleInflight.delete(key)
|
|
titleSubs.get(key)?.forEach(notify => notify(title))
|
|
|
|
return title
|
|
})
|
|
|
|
titleInflight.set(key, promise)
|
|
|
|
return promise
|
|
}
|
|
|
|
export function useSessionLinkTitle(value: string, fallbackLabel?: string): string {
|
|
const key = useMemo(() => sessionRefCacheKey(value), [value])
|
|
const fallback = fallbackLabel?.trim() || sessionRefFallbackLabel(value)
|
|
const [title, setTitle] = useState(() => (key ? titleCache.get(key) || lookupLocalSessionTitle(value) : ''))
|
|
|
|
useEffect(() => {
|
|
if (!key) {
|
|
return
|
|
}
|
|
|
|
const known = titleCache.get(key) || lookupLocalSessionTitle(value)
|
|
|
|
setTitle(known)
|
|
|
|
if (known) {
|
|
return
|
|
}
|
|
|
|
const subs = titleSubs.get(key) ?? new Set<(resolved: string) => void>()
|
|
|
|
subs.add(setTitle)
|
|
titleSubs.set(key, subs)
|
|
void fetchSessionLinkTitle(value)
|
|
|
|
return () => {
|
|
subs.delete(setTitle)
|
|
|
|
if (!subs.size) {
|
|
titleSubs.delete(key)
|
|
}
|
|
}
|
|
}, [key, value])
|
|
|
|
return title || fallback
|
|
}
|
|
|
|
export function __resetSessionLinkTitleCache(): void {
|
|
titleCache.clear()
|
|
titleInflight.clear()
|
|
titleSubs.clear()
|
|
}
|