fix(web-presenter): dim-Markdown web fallback, memoize fetch conversion, strip note residue
Route a `web` result card's raw-content fallback through the TUI's dim Markdown path (render() only recognized `card: 'generic'` as markdown content, so web fallback rendered as bare undimmed text). Memoize renderFetchOutput per (result, maxOutputChars) so the registry's twin output.render / output.presentationMeta calls on the same frozen result run one HTML->markdown conversion instead of two. Remove the trailing `</content>`/`</invoke>` protocol residue from both sides of the web-result-card Agent Note and re-record the pairing.
This commit is contained in:
@@ -266,6 +266,11 @@ interface RenderedFetch {
|
||||
* limits the source prefix processed synchronously, then applies again where the
|
||||
* complete output — header, rendered body, and footer — is known.
|
||||
*
|
||||
* The tool registry calls this once through `output.render` and again through
|
||||
* `output.presentationMeta`, both with the same frozen result value; the
|
||||
* conversion is memoized per `(result, maxOutputChars)` so the synchronous DOM
|
||||
* parse and turndown walk run once, not twice, on the same body.
|
||||
*
|
||||
* @param result - the seam's fetch outcome.
|
||||
* @param maxOutputChars - cap on the complete returned string; a cut body gets
|
||||
* the same fetch-something-narrower notice as provider-side truncation.
|
||||
@@ -273,6 +278,32 @@ interface RenderedFetch {
|
||||
* the provider, a source cut, or the cap trimmed the content.
|
||||
*/
|
||||
export function renderFetchOutput(result: WebFetchResult, maxOutputChars: number): RenderedFetch {
|
||||
const byCap = renderCache.get(result) ?? new Map<number, RenderedFetch>()
|
||||
const cached = byCap.get(maxOutputChars)
|
||||
if (cached !== undefined) return cached
|
||||
const computed = computeFetchOutput(result, maxOutputChars)
|
||||
byCap.set(maxOutputChars, computed)
|
||||
renderCache.set(result, byCap)
|
||||
return computed
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-result memo for {@link renderFetchOutput}, keyed first on the frozen
|
||||
* result value so a garbage-collected result drops its entry, then on the output
|
||||
* cap (a deployment constant per registration). Collapses the registry's twin
|
||||
* `render`/`presentationMeta` calls into one HTML→markdown conversion.
|
||||
*/
|
||||
const renderCache = new WeakMap<WebFetchResult, Map<number, RenderedFetch>>()
|
||||
|
||||
/**
|
||||
* The uncached conversion behind {@link renderFetchOutput}. Separated so the
|
||||
* memo wraps exactly one call site and the conversion logic stays pure.
|
||||
*
|
||||
* @param result - the seam's fetch outcome.
|
||||
* @param maxOutputChars - cap on the complete returned string.
|
||||
* @returns the bounded text and effective truncation.
|
||||
*/
|
||||
function computeFetchOutput(result: WebFetchResult, maxOutputChars: number): RenderedFetch {
|
||||
const header = `Fetched ${result.url} (HTTP ${result.statusCode})\n\n`
|
||||
const rendered = renderBody(result.body, maxOutputChars)
|
||||
const prefix = `${header}${rendered.text}`
|
||||
|
||||
@@ -388,6 +388,27 @@ describe('web_fetch presentation meta and result view', () => {
|
||||
expect(formatFetchOutput(value, NO_CAP)).not.toContain('Content truncated')
|
||||
})
|
||||
|
||||
it('converts one HTML body once across the render and meta projections of the same result', () => {
|
||||
// The registry calls output.render and output.presentationMeta with the same
|
||||
// frozen result value; the memo must collapse them into one turndown walk so
|
||||
// a large or deeply nested page is not parsed and converted twice. A second
|
||||
// cap on the same result is a distinct entry, so it converts again.
|
||||
const spy = vi.spyOn(TurndownService.prototype, 'turndown')
|
||||
const value = {
|
||||
url: 'https://a.test', statusCode: 200, truncated: false,
|
||||
body: { kind: 'html' as const, content: '<p>hello</p>' },
|
||||
}
|
||||
try {
|
||||
formatFetchOutput(value, NO_CAP)
|
||||
fetchMetaFromValue(value, NO_CAP)
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
formatFetchOutput(value, NO_CAP - 1)
|
||||
expect(spy).toHaveBeenCalledTimes(2)
|
||||
} finally {
|
||||
spy.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it('presents a completed fetch as a web/fetch card carrying the summary, titled by the url, without content', () => {
|
||||
const meta = fetchMetaFromValue({ url: 'https://a.test', statusCode: 200, truncated: false, body: { kind: 'text', content: '# Title' } }, NO_CAP)
|
||||
expect(presentFetchResult({ url: 'https://a.test' }, toolResult(meta, '# Title'))).toEqual({
|
||||
|
||||
Reference in New Issue
Block a user