Merge remote-tracking branch 'origin/master' into mergebot/pr1389

# Conflicts:
#	packages/client/ui-workspace/src/client/WorkspaceBrowser.module.css
This commit is contained in:
imccyu
2026-08-04 16:06:38 +08:00
574 changed files with 8022 additions and 29830 deletions

View File

@@ -1,7 +1,8 @@
/* Geometry mirrors CodeBlock/TerminalBlock (12px radius, code-block surface,
16px vertical margin) so a web card, a terminal card, and a fenced code block
read as one family. A source list is prose, not aligned output, so it wraps
normally rather than scrolling horizontally like a terminal card's output. */
read as one family. A source list is prose, not aligned output, so each row
wraps horizontally rather than scrolling sideways like a terminal card; the
list as a whole scrolls vertically within a capped height (see .sources). */
.block {
--dsl-web-radius: 12px;
@@ -27,13 +28,29 @@
margin-bottom: 0;
}
/* The citation list: ordered so each source reads as a numbered reference. */
/* The citation list: ordered so each source reads as a numbered reference. The
whole list — the sources the tool returned, matching what the model saw —
renders here; a max-height caps the card so a long list scrolls in place
rather than growing the card unbounded. The height is a design constant of the
card's geometry, not a deployment choice, so it lives here rather than a plugin
config field.
`overflow-y` makes this a scroll container, which also clips inline-start
overflow: a marker wider than `padding-left` loses its leading digits with no
way to scroll them back. Markers are right-aligned to the content edge, so the
padding must fit the widest one the list can produce. `searchMaxResults` is an
unbounded positive integer, so the padding is sized in `em` — against this
element's own font, the one a marker inherits — to hold a three-digit marker
(`999. ` measures 2.35em in the app font stack) plus the gap the one-digit
case already had. */
.sources {
margin: 0;
padding-left: 20px;
padding-left: 2.5em;
display: flex;
flex-direction: column;
gap: 10px;
max-height: 320px;
overflow-y: auto;
}
.source {
@@ -65,26 +82,6 @@
font: var(--dsw-font-xs-13);
}
.expandItem {
list-style: none;
}
.expand {
display: block;
width: 100%;
padding: 0;
border: none;
background-color: transparent;
color: var(--dsw-alias-label-tertiary);
cursor: pointer;
font: inherit;
text-align: left;
}
.expand:hover {
color: var(--dsw-alias-label-secondary);
}
.truncated {
margin-top: 8px;
color: var(--dsw-alias-label-tertiary);

View File

@@ -9,22 +9,20 @@
// allowlist MarkdownText applies to untrusted assistant-authored links (it also
// permits mailto, excluded here); an unparseable or non-http URL renders as
// plain text. Geometry, radius, and fonts mirror CodeBlock/TerminalBlock so a
// web card reads as one family with them; a long source list caps at maxSources
// with a head/tail collapse using the same arithmetic as TerminalBlock's output
// cap.
// web card reads as one family with them; the whole source list renders inside a
// fixed-height scroll container (its `.sources` max-height), so a long list
// scrolls in place rather than growing the card — and that container's
// `padding-left` must stay wide enough for the widest `<li>` marker, since a
// scroll container clips inline-start overflow irrecoverably. The card draws every source the
// view carries: the tool already cut the list to its source cap, and `truncated`
// reports that cut. A content-only transform downstream of the tool — spill-policy
// replacing an oversized result's text while leaving its presentationMeta whole —
// can still narrow what the model reads below this list.
import { useCallback, useState } from 'react'
import clsx from 'clsx'
import { MarkdownText } from './markdown/MarkdownText.tsx'
import css from './WebBlock.module.css'
/**
* Sources shown before the height cap collapses the middle of a citation list.
* Matches TerminalBlock's default output budget so both cards cut a long body
* at the same place; the chat row narrows it through the maxSources prop.
*/
export const DEFAULT_WEB_MAX_SOURCES = 16
/**
* One citeable source drawn in a search card: the projection of the contract's
* `WebSource`, with the optional fields kept optional so a provider that
@@ -50,8 +48,6 @@ export interface WebSearchBlockProps {
sources: WebSourceView[]
/** True when the tool cut the source list to its result cap. */
truncated: boolean
/** Sources shown before the middle collapses (default {@link DEFAULT_WEB_MAX_SOURCES}). */
maxSources?: number | undefined
/** Extra class merged onto the wrapper (callers position; this component draws). */
className?: string | undefined
}
@@ -65,13 +61,6 @@ export interface WebFetchBlockProps {
statusCode: number
/** True when the provider or the output cap cut the fetched content. */
truncated: boolean
/**
* Accepted and ignored, so both card kinds take one uniform prop set (a fetch
* card has no source list to cap) — the same way TerminalBlock accepts one
* `maxLines` across its arms. Lets a render site spread `maxSources` onto
* either kind without a per-kind conditional.
*/
maxSources?: number | undefined
/** Extra class merged onto the wrapper (callers position; this component draws). */
className?: string | undefined
}
@@ -137,9 +126,9 @@ function SafeLink({ url, label, className }: { url: string; label: string; class
/**
* One source row in a search card: the safe link plus its snippet and date. The
* `<li value>` pins the source's original 1-based position, so a collapsed list
* whose tail is drawn after the head still numbers each source by its real
* citation index rather than by its position in the visible subset.
* `<li value>` pins the source's 1-based citation index explicitly rather than
* relying on the `<ol>`'s implicit numbering, so a row reads by its real index
* even inside the scroll container.
* @param props.source - the source to render.
* @param props.ordinal - the source's 1-based position in the full list.
* @returns the source list item.
@@ -159,21 +148,12 @@ function SourceItem({ source, ordinal }: { source: WebSourceView; ordinal: numbe
}
/**
* The search card body: the answer over the capped source list.
* The search card body: the answer over the full source list, which scrolls in
* place once it exceeds the `.sources` container height.
* @param props - see {@link WebSearchBlockProps}.
* @returns the search card element.
*/
function WebSearchBlock({ answer, sources, truncated, maxSources = DEFAULT_WEB_MAX_SOURCES, className }: WebSearchBlockProps) {
const [expanded, setExpanded] = useState(false)
const onToggle = useCallback(() => { setExpanded(value => !value) }, [])
const hidden = sources.length - maxSources
const capped = hidden > 0 && !expanded
// Same split arithmetic as TerminalBlock's output cap, so a long body's head
// and tail slices agree between the two cards.
const headCount = Math.ceil(maxSources / 2)
const tailCount = maxSources - headCount
const head = capped ? sources.slice(0, headCount) : sources
const tail = capped ? sources.slice(sources.length - tailCount) : []
function WebSearchBlock({ answer, sources, truncated, className }: WebSearchBlockProps) {
// A provider may legitimately return no answer and no sources; the chat WebRow
// does not show the raw result content, so without this the user would see an
// empty card. Mirror the backend's `No results found.` render text.
@@ -187,27 +167,7 @@ function WebSearchBlock({ answer, sources, truncated, maxSources = DEFAULT_WEB_M
<div className={css.empty}></div>
) : (
<ol className={css.sources}>
{head.map((source, index) => <SourceItem key={index} source={source} ordinal={index + 1} />)}
{hidden > 0 && (
<li className={css.expandItem}>
<button
type="button"
className={css.expand}
aria-expanded={expanded}
aria-label={expanded ? '收起来源' : `展开其余 ${hidden} 条来源`}
onClick={onToggle}
>
{expanded ? '收起' : `… 其余 ${hidden} 条来源`}
</button>
</li>
)}
{tail.map((source, index) => (
<SourceItem
key={sources.length - tailCount + index}
source={source}
ordinal={sources.length - tailCount + index + 1}
/>
))}
{sources.map((source, index) => <SourceItem key={index} source={source} ordinal={index + 1} />)}
</ol>
)}
{truncated && <div className={css.truncated}></div>}

View File

@@ -32,7 +32,7 @@ export { SearchBlock, DEFAULT_SEARCH_MAX_LINES } from './SearchBlock.tsx'
export type {
SearchBlockProps, SearchMatchesBlockProps, SearchPathsBlockProps, SearchFileGroup, SearchBlockLineMatch,
} from './SearchBlock.tsx'
export { WebBlock, DEFAULT_WEB_MAX_SOURCES } from './WebBlock.tsx'
export { WebBlock } from './WebBlock.tsx'
export type { WebBlockProps, WebSearchBlockProps, WebFetchBlockProps, WebSourceView } from './WebBlock.tsx'
export { CodeBlock } from './markdown/CodeBlock.tsx'
export type { CodeBlockProps } from './markdown/CodeBlock.tsx'