feat(desktop): show resolved titles on @session chips

Route session refs in the transcript through the title resolver so a
dropped session reads as its title instead of a truncated id, and use
Tabler's funnel for the session chip icon.
This commit is contained in:
Brooklyn Nicholson 2026-07-24 22:16:49 -05:00
parent cbad98e04e
commit dfbc9dbb15
3 changed files with 30 additions and 13 deletions

View file

@ -15,6 +15,7 @@ import {
type SlashChipKind,
slashIconElement
} from '@/components/assistant-ui/directive-text'
import { sessionRefFallbackLabel } from '@/lib/session-link-title'
export const RICH_INPUT_SLOT = 'composer-rich-input'
@ -59,7 +60,9 @@ export function refChipHtml(kind: string, rawValue: string, displayLabel?: strin
const id = unquoteRef(rawValue)
const text = `@${kind}:${quoteRefValue(id)}`
return `<span contenteditable="false" data-ref-text="${escapeHtml(text)}" data-ref-id="${escapeHtml(id)}" data-ref-kind="${escapeHtml(kind)}" class="${DIRECTIVE_CHIP_CLASS}">${directiveIconSvg(kind)}<span class="truncate">${escapeHtml(displayLabel || refLabel(id))}</span></span>`
const label = displayLabel || (kind === 'session' ? sessionRefFallbackLabel(id) : refLabel(id))
return `<span contenteditable="false" data-ref-text="${escapeHtml(text)}" data-ref-id="${escapeHtml(id)}" data-ref-kind="${escapeHtml(kind)}" class="${DIRECTIVE_CHIP_CLASS}">${directiveIconSvg(kind)}<span class="truncate">${escapeHtml(label)}</span></span>`
}
export function refChipElement(kind: string, rawValue: string, displayLabel?: string) {
@ -74,7 +77,7 @@ export function refChipElement(kind: string, rawValue: string, displayLabel?: st
chip.dataset.refKind = kind
chip.className = DIRECTIVE_CHIP_CLASS
label.className = 'truncate'
label.textContent = displayLabel || refLabel(id)
label.textContent = displayLabel || (kind === 'session' ? sessionRefFallbackLabel(id) : refLabel(id))
chip.append(directiveIconElement(kind), label)
return chip

View file

@ -36,4 +36,14 @@ describe('hermesDirectiveFormatter.parse', () => {
{ kind: 'text', text: ' the entry point' }
])
})
it('parses session links with profile/id values', () => {
const segments = hermesDirectiveFormatter.parse('see @session:work/20260101_abc123 next')
expect(segments).toEqual([
{ kind: 'text', text: 'see ' },
{ kind: 'mention', type: 'session', label: '20260101…', id: 'work/20260101_abc123' },
{ kind: 'text', text: ' next' }
])
})
})

View file

@ -8,6 +8,7 @@ import { Fragment, useEffect, useMemo, useState } from 'react'
import { ZoomableImage } from '@/components/chat/zoomable-image'
import { extractEmbeddedImages } from '@/lib/embedded-images'
import { gatewayMediaDataUrl, isRemoteGateway } from '@/lib/media'
import { sessionRefFallbackLabel, useSessionLinkTitle } from '@/lib/session-link-title'
const HERMES_REF_TYPES = ['file', 'folder', 'url', 'image', 'tool', 'line', 'terminal', 'session'] as const
type HermesRefType = (typeof HERMES_REF_TYPES)[number]
@ -40,11 +41,7 @@ const ICON_PATHS: Record<HermesRefType, string[]> = {
tool: ['M7 10h3v-3l-3.5 -3.5a6 6 0 0 1 8 8l6 6a2 2 0 0 1 -3 3l-6 -6a6 6 0 0 1 -8 -8l3.5 3.5'],
line: ['M5 9l14 0', 'M5 15l14 0', 'M11 4l-4 16', 'M17 4l-4 16'],
terminal: ['M5 7l5 5l-5 5', 'M12 19l7 0'],
session: [
'M8 9h8',
'M8 13h6',
'M18 4a3 3 0 0 1 3 3v8a3 3 0 0 1 -3 3h-5l-5 3v-3h-2a3 3 0 0 1 -3 -3v-8a3 3 0 0 1 3 -3z'
]
session: ['M4 4h16v2.172a2 2 0 0 1 -.586 1.414l-4.414 4.414v7l-6 2v-8.5l-4.48 -4.928a2 2 0 0 1 -.52 -1.345v-2.227']
}
const ICON_FALLBACK = ['M8 12a4 4 0 1 0 8 0a4 4 0 1 0 -8 0', 'M16 12v1.5a2.5 2.5 0 0 0 5 0v-1.5a9 9 0 1 0 -5.5 8.28']
@ -314,12 +311,8 @@ function shortLabel(type: HermesRefType, id: string): string {
}
}
// `@session:<profile>/<id>` — show a short id; the composer chip carries the
// friendly title, but once sent the wire form only has the id.
if (type === 'session') {
const sid = id.split('/').filter(Boolean).pop() || id
return sid.length > 10 ? `${sid.slice(0, 8)}` : sid
return sessionRefFallbackLabel(id)
}
const tail = id.split(/[\\/]/).filter(Boolean).pop()
@ -368,7 +361,9 @@ export function DirectiveContent({ text }: { text: string }) {
{segments.map((segment, index) =>
segment.kind === 'text' ? (
<Fragment key={`t-${index}`}>{segment.text}</Fragment>
) : segment.type === 'image' ? null : (
) : segment.type === 'image' ? null : segment.type === 'session' ? (
<SessionDirectiveChip id={segment.id} key={`m-${index}-${segment.id}`} label={segment.label} />
) : (
<DirectiveChip id={segment.id} key={`m-${index}-${segment.id}`} label={segment.label} type={segment.type} />
)
)}
@ -451,6 +446,15 @@ const DirectiveImage: FC<{ id: string; label: string }> = ({ id, label }) => {
)
}
const SessionDirectiveChip: FC<{
label: string
id: string
}> = ({ label, id }) => {
const resolved = useSessionLinkTitle(id, label)
return <DirectiveChip id={id} label={resolved} type="session" />
}
const DirectiveChip: FC<{
type: string
label: string