A user message's image and file blocks now render as one ordered attachment block in the Context browser: images keep their session-authorized thumbnail, other files show their type glyph, extension, and stored size, and the row chip counts every attachment. One click IS the preview — the card hands the attachment's named host path to dsh's own preview column (the File Activity card's `previewAddressOf` → `openResourceVia` path), which renders markdown, code, PDF, office, and image viewers under the file's real name. A refused open reports on that card alone; the image lightbox and the per-type modal it replaced are gone with `images.tsx`. The path comes from the plugin's own `/api/dsh-context/attachment` route (the same authenticated `/api` fence, Connection exact-route registry, and cold-read gate as the detail and fleet routes). It answers only an id the session's own log cites — a depth-bounded, total walk over the event payload (user content, tool-result blocks, spliced inbox deliveries) — and answers the store's named alias: `attachments.fileHostPath` for verbatim files, and a published-or-reused named hard link for images, whose objects are digest-named and suffixless while the preview column dispatches viewers by suffix. The link shares the immutable object bytes and lives in a disposable cache (`<home>/cache/dsh-context/previews/`); a store that is not host-file-backed, or a media type the harness never admits, resolves the typed null the card reports as an unavailable preview. `Section` moves to its own module so the attachment block heads itself without importing the tool card, and the compat matrix probes `FileTypeIcon` alongside the icons it already pins.
159 lines
5.9 KiB
TypeScript
159 lines
5.9 KiB
TypeScript
// The shared attachment vocabulary (src/shared/attachment.ts): both block spellings normalize into
|
|
// one proven reference, and the preview tables classify what a card can show.
|
|
|
|
import assert from 'node:assert/strict'
|
|
import { describe, test } from 'vitest'
|
|
import {
|
|
attachmentExtension,
|
|
attachmentOf,
|
|
attachmentsIn,
|
|
isImageName,
|
|
leafNameOf,
|
|
} from '../../src/shared/attachment'
|
|
|
|
describe('attachmentExtension', () => {
|
|
test('reads the last suffix of a name or path, lower-cased', () => {
|
|
assert.equal(attachmentExtension('report.MD'), 'md')
|
|
assert.equal(attachmentExtension('dir/sub/file.tar.gz'), 'gz')
|
|
assert.equal(attachmentExtension('C:\\docs\\sheet.XLSX'), 'xlsx')
|
|
})
|
|
|
|
test('a missing, trailing, or leading-dot suffix is empty', () => {
|
|
assert.equal(attachmentExtension('README'), '')
|
|
assert.equal(attachmentExtension('archive.'), '')
|
|
assert.equal(attachmentExtension('.env'), 'env')
|
|
assert.equal(attachmentExtension('.gitignore'), 'gitignore')
|
|
assert.equal(attachmentExtension(''), '')
|
|
})
|
|
})
|
|
|
|
describe('isImageName', () => {
|
|
test('matches exactly the suffixes the preview column\'s image viewers claim', () => {
|
|
assert.equal(isImageName('x.PNG'), true)
|
|
assert.equal(isImageName('/dir/shot.webp'), true)
|
|
assert.equal(isImageName('vector.svg'), true)
|
|
assert.equal(isImageName('scan.tiff'), false, 'the column lists tiff as unviewable')
|
|
assert.equal(isImageName('photo.avif'), false, 'the column lists avif as unviewable')
|
|
assert.equal(isImageName('notes.md'), false)
|
|
assert.equal(isImageName('report.docx'), false)
|
|
assert.equal(isImageName(''), false)
|
|
})
|
|
})
|
|
|
|
describe('leafNameOf', () => {
|
|
test('keeps the leaf of either separator style, so a path never escapes its directory', () => {
|
|
assert.equal(leafNameOf('/a/b/report.md'), 'report.md')
|
|
assert.equal(leafNameOf('C:\\tmp\\x.png'), 'x.png')
|
|
assert.equal(leafNameOf('report.md'), 'report.md')
|
|
assert.equal(leafNameOf(''), '')
|
|
})
|
|
})
|
|
|
|
describe('attachmentOf', () => {
|
|
test('rejects every non-attachment block', () => {
|
|
assert.equal(attachmentOf(null), null)
|
|
assert.equal(attachmentOf('image'), null)
|
|
assert.equal(attachmentOf(42), null)
|
|
assert.equal(attachmentOf({}), null)
|
|
assert.equal(attachmentOf({ type: 'text', attachment: { attachmentId: 'a' } }), null)
|
|
assert.equal(attachmentOf({ kind: 'mystery', attachment: { attachmentId: 'a' } }), null)
|
|
})
|
|
|
|
test('rejects a missing or malformed attachment envelope', () => {
|
|
assert.equal(attachmentOf({ type: 'image' }), null)
|
|
assert.equal(attachmentOf({ type: 'image', attachment: null }), null)
|
|
assert.equal(attachmentOf({ type: 'image', attachment: 'x' }), null)
|
|
assert.equal(attachmentOf({ type: 'file', attachment: {} }), null)
|
|
assert.equal(attachmentOf({ type: 'file', attachment: { attachmentId: '' } }), null)
|
|
assert.equal(attachmentOf({ type: 'file', attachment: { attachmentId: 42 } }), null)
|
|
})
|
|
|
|
test('a verbatim file keeps the name and size the log carries', () => {
|
|
assert.deepEqual(
|
|
attachmentOf({ type: 'file', attachment: { attachmentId: 'sha256:x', name: 'notes.md', bytes: 47320 } }),
|
|
{ kind: 'file', attachmentId: 'sha256:x', name: 'notes.md', bytes: 47320 },
|
|
)
|
|
assert.deepEqual(attachmentOf({ kind: 'file', attachment: { attachmentId: 'sha256:x' } }), { kind: 'file', attachmentId: 'sha256:x' })
|
|
assert.deepEqual(
|
|
attachmentOf({ type: 'file', attachment: { attachmentId: 'sha256:x', name: '', bytes: -1 } }),
|
|
{ kind: 'file', attachmentId: 'sha256:x' },
|
|
)
|
|
})
|
|
|
|
test('an image passes every proven fact and drops the hostile ones', () => {
|
|
assert.deepEqual(
|
|
attachmentOf({
|
|
type: 'image',
|
|
attachment: {
|
|
attachmentId: 'sha256:i',
|
|
mediaType: 'image/jpeg',
|
|
name: 'result-image-1.jpg',
|
|
bytes: 146227,
|
|
width: 1568,
|
|
height: 1350,
|
|
originalDimensions: { width: 3462, height: 1211 },
|
|
},
|
|
}),
|
|
{
|
|
kind: 'image',
|
|
attachmentId: 'sha256:i',
|
|
name: 'result-image-1.jpg',
|
|
mediaType: 'image/jpeg',
|
|
bytes: 146227,
|
|
width: 1568,
|
|
height: 1350,
|
|
originalDimensions: { width: 3462, height: 1211 },
|
|
},
|
|
)
|
|
assert.deepEqual(
|
|
attachmentOf({
|
|
kind: 'image',
|
|
attachment: {
|
|
attachmentId: 'sha256:i',
|
|
mediaType: '',
|
|
name: '',
|
|
bytes: '500',
|
|
width: NaN,
|
|
height: 0,
|
|
originalDimensions: 'big',
|
|
},
|
|
}),
|
|
{ kind: 'image', attachmentId: 'sha256:i' },
|
|
)
|
|
assert.deepEqual(
|
|
attachmentOf({ type: 'image', attachment: { attachmentId: 'sha256:i', originalDimensions: null } }),
|
|
{ kind: 'image', attachmentId: 'sha256:i' },
|
|
)
|
|
assert.deepEqual(
|
|
attachmentOf({ type: 'image', attachment: { attachmentId: 'sha256:i', originalDimensions: { width: 0, height: 10 } } }),
|
|
{ kind: 'image', attachmentId: 'sha256:i' },
|
|
)
|
|
assert.deepEqual(
|
|
attachmentOf({ type: 'image', attachment: { attachmentId: 'sha256:i', originalDimensions: { width: 4000, height: 3000 } } }),
|
|
{ kind: 'image', attachmentId: 'sha256:i', originalDimensions: { width: 4000, height: 3000 } },
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('attachmentsIn', () => {
|
|
test('keeps the message order and drops everything else', () => {
|
|
const content = [
|
|
{ type: 'file', attachment: { attachmentId: 'f1', name: 'a.svg', bytes: 2627 } },
|
|
{ type: 'text', text: 'hello' },
|
|
{ type: 'image', attachment: { attachmentId: 'i1', width: 10, height: 10 } },
|
|
null,
|
|
'plain',
|
|
{ type: 'file' },
|
|
]
|
|
assert.deepEqual(attachmentsIn(content), [
|
|
{ kind: 'file', attachmentId: 'f1', name: 'a.svg', bytes: 2627 },
|
|
{ kind: 'image', attachmentId: 'i1', width: 10, height: 10 },
|
|
])
|
|
})
|
|
|
|
test('a non-array content has none', () => {
|
|
assert.deepEqual(attachmentsIn(undefined), [])
|
|
assert.deepEqual(attachmentsIn('content'), [])
|
|
})
|
|
})
|