Files
hermes-agent/apps/desktop/src/lib/transcript-directives.test.ts
T
Brooklyn Nicholson 59b1c40cdf feat(desktop): plugins can render inline components in assistant messages via ::name{...} directives
The transcript becomes a contribution area (transcript.directives). A plugin
registers a named directive and the model addresses it by emitting
::name{key="value"} as its own paragraph; that leaf renders as the plugin's
component, wrapped in the contribution error boundary. Unclaimed or malformed
directives stay plain prose, so nothing changes for text that merely looks
like a directive (std::vector) or for users with the plugin disabled.

Core ships ::preview{file="..."} as the reference consumer (the existing
preview-attachment card), the desktop platform hint teaches the model the
syntax, and the SDK exports the area + types so runtime plugin.js files get
the surface through the normal plugins API.
2026-08-17 15:12:53 -05:00

55 lines
1.9 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { parseTranscriptDirective } from './transcript-directives'
describe('parseTranscriptDirective', () => {
it('parses a bare directive with no attributes', () => {
expect(parseTranscriptDirective('::tasks')).toEqual({ name: 'tasks', attrs: {}, source: '::tasks' })
})
it('parses double-quoted attributes', () => {
expect(parseTranscriptDirective('::preview{file="demo.html"}')).toEqual({
name: 'preview',
attrs: { file: 'demo.html' },
source: '::preview{file="demo.html"}'
})
})
it('parses multiple attributes and accepts single quotes', () => {
expect(parseTranscriptDirective(`::vis{file="a b.html" height='480'}`)?.attrs).toEqual({
file: 'a b.html',
height: '480'
})
})
it('lowercases attribute keys but preserves values', () => {
expect(parseTranscriptDirective('::vis{File="A.html"}')?.attrs).toEqual({ file: 'A.html' })
})
it('tolerates surrounding whitespace', () => {
expect(parseTranscriptDirective(' ::tasks{id="1"} ')?.name).toBe('tasks')
})
it('rejects prose containing a directive mid-text', () => {
expect(parseTranscriptDirective('see ::preview{file="x.html"} above')).toBeNull()
})
it('rejects multi-line paragraphs', () => {
expect(parseTranscriptDirective('::preview{file="x.html"}\nmore')).toBeNull()
})
it('rejects C++ scope-resolution lookalikes', () => {
expect(parseTranscriptDirective('::std')).toEqual({ name: 'std', attrs: {}, source: '::std' })
expect(parseTranscriptDirective('std::vector<int>')).toBeNull()
expect(parseTranscriptDirective('::Vector')).toBeNull()
})
it('rejects unquoted attribute values', () => {
expect(parseTranscriptDirective('::preview{file=demo.html}')?.attrs).toEqual({})
})
it('bounds pathological input instead of scanning it', () => {
expect(parseTranscriptDirective(`::x{${'a="b" '.repeat(400)}}`)).toBeNull()
})
})