99ab036291
Asked to link to a session, the agent had no way to know the @session reference syntax exists — every mention in the tool schema described consuming a link the user dropped, never writing one — so it answered with the title and timestamp as prose and the desktop had nothing to render. Every result now carries a ready-to-copy `link`, and the schema says to write it inline instead of restating the title around it. The profile segment is omitted when the active profile can't be named confidently; a bare id still resolves. Also skip linkifying a ref a model already wrapped in a markdown link, which would otherwise rewrite into a nested link.
119 lines
3.7 KiB
TypeScript
119 lines
3.7 KiB
TypeScript
/**
|
|
* Pure helpers for `@session:<profile>/<id>` references.
|
|
*
|
|
* Kept free of React/store/API imports so the markdown preprocessor (a hot
|
|
* per-flush path) can use them without pulling in the resolver; the stateful
|
|
* title lookup lives in `session-link-title.ts`.
|
|
*/
|
|
|
|
/** Mirrors the composer/transcript form in `directive-text.tsx`: a bare value,
|
|
* or one fenced in backticks/quotes so a value with spaces survives. The
|
|
* lookbehinds keep `foo@session:` and URL paths from matching, and skip a ref
|
|
* a model already wrapped in a markdown link — rewriting that would nest. */
|
|
export const SESSION_REF_RE = /(?<![\w/])(?<!]\()@session:(`[^`\n]+`|"[^"\n]+"|'[^'\n]+'|\S+)/g
|
|
|
|
const TRAILING_PUNCTUATION_RE = /[,.;:!?)\]}]+$/
|
|
|
|
function unwrapQuotes(raw: string): null | string {
|
|
if (raw.length < 2) {
|
|
return null
|
|
}
|
|
|
|
const head = raw[0]
|
|
const tail = raw[raw.length - 1]
|
|
|
|
if ((head === '`' && tail === '`') || (head === '"' && tail === '"') || (head === "'" && tail === "'")) {
|
|
return raw.slice(1, -1)
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
/** Splits a matched value into the reference itself and any prose punctuation
|
|
* that the greedy `\S+` branch swallowed. Quoted values are already fenced. */
|
|
export function splitSessionRefValue(raw: string): { trailing: string; value: string } {
|
|
const quoted = unwrapQuotes(raw)
|
|
|
|
if (quoted !== null) {
|
|
return { trailing: '', value: quoted }
|
|
}
|
|
|
|
const value = raw.replace(TRAILING_PUNCTUATION_RE, '')
|
|
|
|
return { trailing: raw.slice(value.length), value }
|
|
}
|
|
|
|
/** Session ids never contain a slash, so a slash unambiguously means
|
|
* `<profile>/<id>` — same split as `tools/session_search_tool.py`. */
|
|
export function parseSessionRefValue(value: string): { profile?: string; sessionId: string } {
|
|
const trimmed = value.trim()
|
|
const slash = trimmed.indexOf('/')
|
|
|
|
if (slash === -1) {
|
|
return { sessionId: trimmed }
|
|
}
|
|
|
|
const profile = trimmed.slice(0, slash).trim()
|
|
const sessionId = trimmed.slice(slash + 1).trim()
|
|
|
|
return sessionId ? { profile: profile || undefined, sessionId } : { sessionId: trimmed }
|
|
}
|
|
|
|
export function sessionRefCacheKey(value: string): string {
|
|
const { profile, sessionId } = parseSessionRefValue(value)
|
|
|
|
return sessionId ? `${profile ?? ''}/${sessionId}` : ''
|
|
}
|
|
|
|
/** Chip label before (or without) a resolved title — a short, still-identifying id. */
|
|
export function sessionRefFallbackLabel(value: string): string {
|
|
const { sessionId } = parseSessionRefValue(value)
|
|
|
|
if (!sessionId) {
|
|
return value
|
|
}
|
|
|
|
return sessionId.length > 10 ? `${sessionId.slice(0, 8)}…` : sessionId
|
|
}
|
|
|
|
/** A fragment href, matching the `#preview/` convention in `preview-targets.ts`
|
|
* — no custom URL scheme to survive markdown sanitization. */
|
|
export function sessionMarkdownHref(value: string): string {
|
|
return `#session/${encodeURIComponent(value)}`
|
|
}
|
|
|
|
export function sessionRefFromMarkdownHref(href?: string): null | string {
|
|
if (!href?.startsWith('#session/')) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
return decodeURIComponent(href.slice('#session/'.length)) || null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Rewrites bare `@session:<profile>/<id>` tokens into markdown links so an
|
|
* agent-authored reference reaches `MarkdownLink` and renders as a chip.
|
|
* Callers must exclude code spans/fences — `preprocessMarkdown` already does.
|
|
*/
|
|
export function linkifySessionRefs(text: string): string {
|
|
if (!text.includes('@session:')) {
|
|
return text
|
|
}
|
|
|
|
return text.replace(SESSION_REF_RE, (match, raw: string) => {
|
|
const { trailing, value } = splitSessionRefValue(raw)
|
|
|
|
if (!parseSessionRefValue(value).sessionId) {
|
|
return match
|
|
}
|
|
|
|
const label = sessionRefFallbackLabel(value).replace(/[[\]\\]/g, '\\$&')
|
|
|
|
return `[${label}](${sessionMarkdownHref(value)})${trailing}`
|
|
})
|
|
}
|