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.
53 lines
2.4 KiB
TypeScript
53 lines
2.4 KiB
TypeScript
// The defensive icon resolver (src/client/primitives.ts): each ui-primitives icon resolves by its export name;
|
|
// a name missing from the module (a future rename) or a namespace that throws on the read degrades to a
|
|
// render-nothing component instead of the React #130 element-type crash.
|
|
|
|
import assert from 'node:assert/strict'
|
|
import { describe, test } from 'vitest'
|
|
import { FileTypeGlyph, resolveFileGlyph, resolveIcon, IconBranch, IconPlus, IconCheck, IconCopy, IconClose, IconSettings, IconChevronDown } from '../../src/client/primitives'
|
|
|
|
describe('resolveIcon', () => {
|
|
const icon = function SomeIcon(): null { return null }
|
|
|
|
test('a present export resolves to the component', () => {
|
|
assert.equal(resolveIcon('icon', { icon }), icon)
|
|
})
|
|
|
|
test('a name present but not a component renders nothing', () => {
|
|
const resolved = resolveIcon('icon', { icon: 42 })
|
|
assert.equal(resolved({}), null, 'the fallback component renders null')
|
|
})
|
|
|
|
test('a missing name renders nothing (a future rename)', () => {
|
|
const resolved = resolveIcon('IconGhostOutlineRegular', {})
|
|
assert.equal(resolved({}), null)
|
|
})
|
|
|
|
test('a namespace that throws on property access degrades to the fallback', () => {
|
|
// Partial module mocks and hostile namespaces throw on the READ itself.
|
|
const hostile = new Proxy({}, { get() { throw new Error('hostile export read') } })
|
|
const resolved = resolveIcon('IconBranchOutlineRegular', hostile)
|
|
assert.equal(resolved({}), null)
|
|
})
|
|
|
|
test('the module-level exports resolve against the pinned generation', () => {
|
|
// The devDep carries the supported vocabulary, so these resolved at module load.
|
|
for (const icon of [IconBranch, IconPlus, IconCheck, IconCopy, IconClose, IconSettings, IconChevronDown]) {
|
|
assert.equal(typeof icon, 'function', 'every seam resolves to a component')
|
|
}
|
|
assert.equal(typeof FileTypeGlyph, 'function', 'the file-type glyph resolves too')
|
|
})
|
|
})
|
|
|
|
describe('resolveFileGlyph', () => {
|
|
const glyph = function SomeGlyph(): null { return null }
|
|
|
|
test('a present export resolves; a missing, non-component, or hostile read renders nothing', () => {
|
|
assert.equal(resolveFileGlyph({ FileTypeIcon: glyph }), glyph)
|
|
assert.equal(resolveFileGlyph({ FileTypeIcon: 42 })({}), null)
|
|
assert.equal(resolveFileGlyph({})({}), null)
|
|
const hostile = new Proxy({}, { get() { throw new Error('hostile export read') } })
|
|
assert.equal(resolveFileGlyph(hostile)({}), null)
|
|
})
|
|
})
|