fix(web): address web card review — panel body, ol numbering, safe links, docs

- DetailsPanel: render the flattened result content below the web card, so a
  web_fetch's fetched body (and a search's answer/source markdown) stays
  visible on the panel's single-call reading surface — the card is a summary.
- WebBlock: the collapsed source tail keeps each source's original citation
  number via <li value>, and the expand control is a marker-less <li> so the
  <ol> is valid HTML; an empty-hostname URL (file:/data:) falls back to the raw
  URL so a label is never blank.
- web-row / GenericToolCard: both spread WebBlock uniformly with maxSources
  (fetch ignores it, like TerminalBlock's maxLines), dropping the duplicated
  per-kind conditional.
- Docs: WebBlock added to the ui-primitives README (both languages) with a Web
  retrieval section; the ui-conversation README's "inline licensed for this
  intent alone" claim de-absolutized and a web-card paragraph added; the Agent
  Note's safe-link description corrected to the http(s) subset of MarkdownText's
  allowlist (mailto excluded). Fixture source comment aligned with its data.
- Tests: ol numbering + marker-less expander, empty-hostname label fallback,
  the fetched body visible in the panel.
This commit is contained in:
Chinesezjc
2026-07-30 19:36:17 +08:00
parent 2646d67faa
commit 17ee269f11
17 changed files with 132 additions and 53 deletions
@@ -56,10 +56,7 @@ export function GenericToolCard({ toolName, block, cwd, openFile }: ToolRowOwner
return (
<div className={css.card}>
{row}
<WebBlock
{...(web.kind === 'search' ? { ...web, maxSources: CHAT_WEB_MAX_SOURCES } : web)}
className={css.web}
/>
<WebBlock {...web} maxSources={CHAT_WEB_MAX_SOURCES} className={css.web} />
</div>
)
}
@@ -152,8 +152,20 @@ function OutputBody({ material, cwd }: { material: CallMaterial; cwd: string | u
}
const web = webCardModel(material.block)
// Full source-list allowance here (the panel is the single-call reading
// surface); the chat rows cap it at CHAT_WEB_MAX_SOURCES.
if (web !== null) return <WebBlock {...web} className={css.web} />
// surface); the chat rows cap it at CHAT_WEB_MAX_SOURCES. The card is a
// summary — a web_fetch card shows only the URL and status — so the details
// panel also renders the flattened result content below it (the fetched body,
// the search answer + source markdown), which the card does not carry.
if (web !== null) {
const settled = 'kind' in material.block ? material.block : null
const body = settled === null ? '' : renderResult(settled)
return (
<>
<WebBlock {...web} className={css.web} />
{body !== '' && <pre className={css.code}>{body}</pre>}
</>
)
}
// A settled call always carries the result node the flattened form needs;
// the running shape has no result to flatten.
if (!('kind' in material.block)) return <div className={css.empty}>运行中…</div>
@@ -66,10 +66,7 @@ export function WebRow({ toolName, block }: ToolRowProps) {
<span className={css.summary}>{model.summary}</span>
</div>
{web !== null && (
<WebBlock
{...(web.kind === 'search' ? { ...web, maxSources: CHAT_WEB_MAX_SOURCES } : web)}
className={css.web}
/>
<WebBlock {...web} maxSources={CHAT_WEB_MAX_SOURCES} className={css.web} />
)}
</div>
)