refactor(renderer): give session images one unavailable state, off the DOM
A source the Markdown renderer refuses now goes through the same element with no src rather than a separate light-DOM span, so a blocked source and a source that fails to load are one piece of UI instead of two. That drops the span's stylesheet rule and the only reason the renderer built elements just to serialise them. Decoding what marked escaped no longer runs untrusted markup through a detached element's innerHTML; the entities marked actually emits are decoded in one pass, and attributes are escaped on the way out. With the DOM dependency gone the renderer is directly unit-testable, so the escaping and the protocol allowlist now have coverage that does not need Electron. The element's shadow styles also stop hard-coding values that already exist as tokens -- custom properties cross the shadow boundary, so --muted and --hairline-strong are now the single source of truth -- and the height cap becomes --session-image-max-block, which compact Markdown surfaces (subagent panes, memory rows, tool results) lower from a viewport fraction to 240px. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a9e6d1bab8
commit
1124758b6b
@@ -1,6 +1,9 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { normalizeMarkdownImageToken } from '../app/src/renderer/src/markdown-image-renderer.js';
|
||||
import {
|
||||
normalizeMarkdownImageToken,
|
||||
renderSessionMarkdownImage,
|
||||
} from '../app/src/renderer/src/markdown-image-renderer.js';
|
||||
|
||||
// marked <= 14 calls renderer.image(href, title, text); marked >= 15 passes the
|
||||
// token. Getting this wrong degrades silently: every session image turns into
|
||||
@@ -34,3 +37,56 @@ test('fills in the fields marked leaves null', () => {
|
||||
{ href: 'http://example.test/a.png', title: '', text: '' },
|
||||
);
|
||||
});
|
||||
|
||||
test('renders an allowed source as the session image element', () => {
|
||||
assert.equal(
|
||||
renderSessionMarkdownImage('http://example.test/a.png', '', 'Alt text'),
|
||||
'<obelisk-session-image src="http://example.test/a.png" alt="Alt text"></obelisk-session-image>',
|
||||
);
|
||||
assert.equal(
|
||||
renderSessionMarkdownImage('data:image/png;base64,AAAA', '', ''),
|
||||
'<obelisk-session-image src="data:image/png;base64,AAAA" alt=""></obelisk-session-image>',
|
||||
);
|
||||
});
|
||||
|
||||
test('carries the title through when marked supplies one', () => {
|
||||
assert.equal(
|
||||
renderSessionMarkdownImage('file:///shots/a.png', 'A title', 'Alt'),
|
||||
'<obelisk-session-image src="file:///shots/a.png" alt="Alt" title="A title"></obelisk-session-image>',
|
||||
);
|
||||
});
|
||||
|
||||
test('drops a source the app will not load, keeping one unavailable state', () => {
|
||||
for (const href of ['javascript:alert(1)', 'data:text/html,<b>x</b>', 'ftp://example.test/a.png', '']) {
|
||||
assert.equal(
|
||||
renderSessionMarkdownImage(href, '', 'Alt text'),
|
||||
'<obelisk-session-image alt="Alt text"></obelisk-session-image>',
|
||||
`expected ${href || '(empty)'} to render without a src`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('decodes what marked escaped exactly once, then re-escapes it', () => {
|
||||
assert.equal(
|
||||
renderSessionMarkdownImage('http://example.test/a.png?x=1&y=2', '', '"quoted" & 'single''),
|
||||
'<obelisk-session-image src="http://example.test/a.png?x=1&y=2"'
|
||||
+ ' alt=""quoted" & \'single\'"></obelisk-session-image>',
|
||||
);
|
||||
// A single decoding pass, so text that was literally `<` in the source
|
||||
// does not decay into a real angle bracket.
|
||||
assert.equal(
|
||||
renderSessionMarkdownImage('http://example.test/a.png', '', '&lt;script&gt;'),
|
||||
'<obelisk-session-image src="http://example.test/a.png"'
|
||||
+ ' alt="&lt;script&gt;"></obelisk-session-image>',
|
||||
);
|
||||
});
|
||||
|
||||
test('never lets alt text break out of the attribute', () => {
|
||||
const html = renderSessionMarkdownImage('http://example.test/a.png', '', '"><img src=x onerror=alert(1)>');
|
||||
assert.equal(html.includes('onerror=alert(1)>'), false);
|
||||
assert.equal(
|
||||
html,
|
||||
'<obelisk-session-image src="http://example.test/a.png"'
|
||||
+ ' alt=""><img src=x onerror=alert(1)>"></obelisk-session-image>',
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user