feat(desktop): open the session a @session ref names

An agent-written ref rendered as a chip that went nowhere on click. It now
renders as an ordinary inline link — the agent wrote it mid-sentence, so it
should read like one — with the funnel icon leading the resolved title.
Clicking either surface (that link, or the chip in the user's own message)
opens the session as a tab, the way its sidebar row does.

The tile store loads on click rather than at import: the composer's rich
editor pulls this module in, so a static import would boot the profile store
and its REST routing along with every transcript render.
This commit is contained in:
Brooklyn Nicholson 2026-07-25 12:09:36 -05:00
parent 99ab036291
commit 9edbc68d0f
4 changed files with 132 additions and 28 deletions

View file

@ -7,9 +7,11 @@ import { Fragment, useEffect, useMemo, useState } from 'react'
import { ZoomableImage } from '@/components/chat/zoomable-image'
import { extractEmbeddedImages } from '@/lib/embedded-images'
import { triggerHaptic } from '@/lib/haptics'
import { gatewayMediaDataUrl, isRemoteGateway } from '@/lib/media'
import { useSessionLinkTitle } from '@/lib/session-link-title'
import { sessionRefFallbackLabel } from '@/lib/session-refs'
import { parseSessionRefValue, sessionRefFallbackLabel } from '@/lib/session-refs'
import { cn } from '@/lib/utils'
const HERMES_REF_TYPES = ['file', 'folder', 'url', 'image', 'tool', 'line', 'terminal', 'session'] as const
type HermesRefType = (typeof HERMES_REF_TYPES)[number]
@ -121,9 +123,12 @@ export function slashIconElement(kind: SlashChipKind) {
return iconElementFromPaths(SLASH_ICON_PATHS[kind])
}
const DirectiveIcon: FC<{ type: string }> = ({ type }) => (
const DirectiveIcon: FC<{ type: string; className?: string }> = ({
type,
className = 'size-3 shrink-0 opacity-80'
}) => (
<svg
className="size-3 shrink-0 opacity-80"
className={className}
fill="none"
stroke="currentColor"
strokeLinecap="round"
@ -447,31 +452,87 @@ const DirectiveImage: FC<{ id: string; label: string }> = ({ id, label }) => {
)
}
/** A `@session:<profile>/<id>` reference, rendered as the session's title once
* resolved. Shared by the user transcript (directive segments) and assistant
* markdown (`#session/` links rewritten in `preprocessMarkdown`). */
/** Opens the referenced session as a tab same as middle-clicking its sidebar
* row. The tile store loads on click, not at import: the composer's rich
* editor pulls this module in, and a static import would boot the profile
* store (and its REST routing) along with it. */
function openSessionRef(value: string) {
const { sessionId } = parseSessionRefValue(value)
if (!sessionId) {
return
}
triggerHaptic('selection')
void import('@/store/session-states').then(({ openSessionTile }) => openSessionTile(sessionId, 'center'))
}
/** A `@session:<profile>/<id>` reference in the user transcript (directive
* segments), rendered as a chip like the other composer refs. Clicking it
* opens the session as a tab. */
export const SessionRefChip: FC<{
label?: string
value: string
}> = ({ label, value }) => {
const resolved = useSessionLinkTitle(value, label)
return <DirectiveChip id={value} label={resolved} type="session" />
return <DirectiveChip id={value} label={resolved} onClick={() => openSessionRef(value)} type="session" />
}
/** A `@session:` reference in assistant markdown (`#session/` links rewritten
* in `preprocessMarkdown`). Reads as an ordinary inline link the agent wrote
* it mid-sentence with the funnel icon leading the resolved title. */
export const SessionRefLink: FC<{
label?: string
value: string
}> = ({ label, value }) => {
const resolved = useSessionLinkTitle(value, label)
return (
<a
className="font-semibold text-foreground underline underline-offset-4 decoration-current/20 wrap-anywhere"
href="#"
onClick={event => {
event.preventDefault()
event.stopPropagation()
openSessionRef(value)
}}
title={value}
>
<DirectiveIcon className="mr-1 inline size-[0.82em] align-[-0.08em] opacity-70" type="session" />
{resolved}
</a>
)
}
/** Inert by default; `onClick` promotes the chip to a real button (session
* refs, which open the session they name). */
const DirectiveChip: FC<{
type: string
label: string
id: string
}> = ({ type, label, id }) => (
<span
className={DIRECTIVE_CHIP_CLASS}
data-directive-id={id}
data-directive-type={type}
data-slot="aui_directive-chip"
title={id}
>
<DirectiveIcon type={type} />
<span className="truncate">{label}</span>
</span>
)
onClick?: () => void
}> = ({ type, label, id, onClick }) => {
const body = (
<>
<DirectiveIcon type={type} />
<span className="truncate">{label}</span>
</>
)
const props = {
className: cn(DIRECTIVE_CHIP_CLASS, onClick && 'cursor-pointer transition-colors hover:text-foreground'),
'data-directive-id': id,
'data-directive-type': type,
'data-slot': 'aui_directive-chip',
title: id
}
return onClick ? (
<button {...props} onClick={onClick} type="button">
{body}
</button>
) : (
<span {...props}>{body}</span>
)
}

View file

@ -35,26 +35,26 @@ afterEach(() => {
// End-to-end for the agent-authored path: a bare ref in assistant markdown has
// to survive preprocessMarkdown -> Streamdown -> MarkdownLink and come out as
// the session chip, not as literal text or a dead anchor.
// an inline link titled after the session, not as literal text.
describe('MarkdownTextContent session refs', () => {
it('renders an agent-written @session ref as a chip showing the session title', async () => {
it('renders an agent-written @session ref as a link showing the session title', async () => {
$sessions.set([makeSession()])
render(<MarkdownTextContent isRunning={false} text="Context lives in @session:work/20260101_abc123 today." />)
const chip = await screen.findByTitle('work/20260101_abc123')
const link = await screen.findByTitle('work/20260101_abc123')
expect(chip.dataset.directiveType).toBe('session')
expect(chip.textContent).toBe('Branch plan')
expect(link.tagName).toBe('A')
expect(link.textContent).toBe('Branch plan')
expect(screen.queryByText(/@session:/)).toBeNull()
})
it('falls back to a short id when the session is unknown', async () => {
render(<MarkdownTextContent isRunning={false} text="See @session:work/20260101_abc123 for context." />)
const chip = await screen.findByTitle('work/20260101_abc123')
const link = await screen.findByTitle('work/20260101_abc123')
expect(chip.textContent).toBe('20260101…')
expect(link.textContent).toBe('20260101…')
})
it('leaves a ref inside inline code as literal text', () => {

View file

@ -33,7 +33,7 @@ import { previewTargetFromMarkdownHref } from '@/lib/preview-targets'
import { sessionRefFromMarkdownHref } from '@/lib/session-refs'
import { cn } from '@/lib/utils'
import { SessionRefChip } from './directive-text'
import { SessionRefLink } from './directive-text'
import { detectEmbed, extractAlert, MarkdownAlert, RichCodeBlock, UrlEmbed } from './embeds'
// Math rendering plugin (KaTeX). Configured once at module scope — the
@ -238,7 +238,7 @@ function MarkdownLink({ children, className, href, ...props }: ComponentProps<'a
const sessionRef = sessionRefFromMarkdownHref(href)
if (sessionRef) {
return <SessionRefChip value={sessionRef} />
return <SessionRefLink value={sessionRef} />
}
const target = href ? normalizeExternalUrl(href) : href

View file

@ -0,0 +1,43 @@
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { __resetSessionLinkTitleCache } from '@/lib/session-link-title'
import { DirectiveContent } from './directive-text'
import { MarkdownTextContent } from './markdown-text'
const openSessionTile = vi.fn()
vi.mock('@/store/session-states', () => ({
openSessionTile: (...args: unknown[]) => openSessionTile(...args)
}))
afterEach(() => {
cleanup()
openSessionTile.mockClear()
__resetSessionLinkTitleCache()
})
// Both surfaces render a session ref differently — an inline link in agent
// prose, a chip in the user's own message — but either one opens the session
// it names, the way its sidebar row would.
describe('session refs open the session', () => {
it('opens the session from an agent-written link', async () => {
render(<MarkdownTextContent isRunning={false} text="Picked up in @session:work/20260101_abc123 last night." />)
fireEvent.click(await screen.findByTitle('work/20260101_abc123'))
await vi.waitFor(() => expect(openSessionTile).toHaveBeenCalledWith('20260101_abc123', 'center'))
})
it('opens the session from a chip in the user transcript', async () => {
render(<DirectiveContent text="pick up @session:work/20260101_abc123 please" />)
const chip = screen.getByTitle('work/20260101_abc123')
expect(chip.tagName).toBe('BUTTON')
fireEvent.click(chip)
await vi.waitFor(() => expect(openSessionTile).toHaveBeenCalledWith('20260101_abc123', 'center'))
})
})