Studio: fix Deep Research SSE framing, source counts, and favicon privacy
- Normalize the whole SSE buffer so a CRLF split across transport chunks still frames events. - Count web and document sources together in the activity header so a RAG-only run is not shown as zero sources. - Cap the plan editor at the run's configured maxSteps instead of a hard-coded 30. - Add an allowRemoteIcons opt-out to the sources components and disable third-party favicon requests for research sources so visited domains are not leaked.
This commit is contained in:
parent
5c129f0380
commit
e75a7683a7
4 changed files with 30 additions and 12 deletions
|
|
@ -40,14 +40,16 @@ function SourceIcon({
|
|||
url,
|
||||
className,
|
||||
size = 3,
|
||||
allowRemoteIcons = true,
|
||||
...props
|
||||
}: ComponentProps<"span"> & { url: string; size?: number }) {
|
||||
}: ComponentProps<"span"> & { url: string; size?: number; allowRemoteIcons?: boolean }) {
|
||||
const [hasError, setHasError] = useState(false);
|
||||
const domain = extractDomain(url);
|
||||
const SIZE_CLASSES: Record<number, string> = { 3: "size-3", 4: "size-4", 5: "size-5" };
|
||||
const sizeClass = SIZE_CLASSES[size] ?? "size-3";
|
||||
|
||||
if (hasError) {
|
||||
// When disabled, render the letter fallback instead of fetching a third-party favicon.
|
||||
if (hasError || !allowRemoteIcons) {
|
||||
return (
|
||||
<span
|
||||
data-slot="source-icon-fallback"
|
||||
|
|
@ -137,7 +139,10 @@ export interface SourceData {
|
|||
description?: string;
|
||||
}
|
||||
|
||||
const SourceBadge: FC<{ source: SourceData }> = ({ source }) => {
|
||||
const SourceBadge: FC<{ source: SourceData; allowRemoteIcons?: boolean }> = ({
|
||||
source,
|
||||
allowRemoteIcons = true,
|
||||
}) => {
|
||||
const domain = extractDomain(source.url);
|
||||
const displayTitle = source.title || domain;
|
||||
|
||||
|
|
@ -146,7 +151,7 @@ const SourceBadge: FC<{ source: SourceData }> = ({ source }) => {
|
|||
<HoverCardTrigger asChild>
|
||||
<span className="inline-block">
|
||||
<Source href={source.url}>
|
||||
<SourceIcon url={source.url} />
|
||||
<SourceIcon url={source.url} allowRemoteIcons={allowRemoteIcons} />
|
||||
<SourceTitle>{displayTitle}</SourceTitle>
|
||||
</Source>
|
||||
</span>
|
||||
|
|
@ -158,7 +163,12 @@ const SourceBadge: FC<{ source: SourceData }> = ({ source }) => {
|
|||
style={{ animation: "none" }}
|
||||
>
|
||||
<div className="flex gap-2.5">
|
||||
<SourceIcon url={source.url} size={4} className="mt-0.5 shrink-0" />
|
||||
<SourceIcon
|
||||
url={source.url}
|
||||
size={4}
|
||||
className="mt-0.5 shrink-0"
|
||||
allowRemoteIcons={allowRemoteIcons}
|
||||
/>
|
||||
<div className="min-w-0 space-y-1">
|
||||
<p className="text-sm font-semibold leading-tight truncate">
|
||||
{source.title || domain}
|
||||
|
|
@ -178,8 +188,9 @@ const SourceBadge: FC<{ source: SourceData }> = ({ source }) => {
|
|||
|
||||
// ── Grouped sources with 2-row collapse ─────────────────────
|
||||
|
||||
const SourcesGroup: FC<{ sources?: SourceData[] }> = ({
|
||||
const SourcesGroup: FC<{ sources?: SourceData[]; allowRemoteIcons?: boolean }> = ({
|
||||
sources: suppliedSources,
|
||||
allowRemoteIcons = true,
|
||||
}) => {
|
||||
const message = useMessage();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
|
@ -280,7 +291,7 @@ const SourcesGroup: FC<{ sources?: SourceData[] }> = ({
|
|||
{sources.map((source) => (
|
||||
<span key={source.id} className="inline-block">
|
||||
<Source href={source.url}>
|
||||
<SourceIcon url={source.url} />
|
||||
<SourceIcon url={source.url} allowRemoteIcons={allowRemoteIcons} />
|
||||
<SourceTitle>{source.title || extractDomain(source.url)}</SourceTitle>
|
||||
</Source>
|
||||
</span>
|
||||
|
|
@ -291,7 +302,7 @@ const SourcesGroup: FC<{ sources?: SourceData[] }> = ({
|
|||
{/* Visible container */}
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{displayedSources.map((source) => (
|
||||
<SourceBadge key={source.id} source={source} />
|
||||
<SourceBadge key={source.id} source={source} allowRemoteIcons={allowRemoteIcons} />
|
||||
))}
|
||||
{shouldCollapse && !expanded && (
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -161,7 +161,9 @@ export async function* streamResearchEvents(
|
|||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
buffer += decoder.decode(value, { stream: !done }).replace(/\r\n/g, "\n");
|
||||
buffer += decoder.decode(value, { stream: !done });
|
||||
// Normalize on the whole buffer so a CRLF split across chunks still frames.
|
||||
buffer = buffer.replace(/\r\n/g, "\n");
|
||||
let boundary = buffer.indexOf("\n\n");
|
||||
while (boundary >= 0) {
|
||||
const block = buffer.slice(0, boundary);
|
||||
|
|
|
|||
|
|
@ -633,7 +633,7 @@ function PlanReview({ runId }: { runId: string }): ReactElement | null {
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
disabled={draft.steps.length >= 30}
|
||||
disabled={draft.steps.length >= (run?.config?.budgets?.maxSteps ?? 30)}
|
||||
onClick={() => {
|
||||
setStepKeys((keys) => [
|
||||
...keys,
|
||||
|
|
@ -787,6 +787,11 @@ export function ResearchActivityPanel({
|
|||
}
|
||||
const { run, activities } = session;
|
||||
const elapsedEnd = run.completedAt ?? elapsedNow ?? run.updatedAt;
|
||||
// Count web and document sources together so a RAG-only run is not shown as 0.
|
||||
const documentCount = new Set(
|
||||
(run.documentSources ?? []).map((source) => source.documentId ?? source.filename),
|
||||
).size;
|
||||
const sourceCount = run.sources.length + documentCount;
|
||||
const allowedDomains = run.config?.websitePolicy?.allowedDomains ?? [];
|
||||
const blockedDomains = run.config?.websitePolicy?.blockedDomains ?? [];
|
||||
const websiteLimitLabel = allowedDomains.length
|
||||
|
|
@ -857,7 +862,7 @@ export function ResearchActivityPanel({
|
|||
</p>
|
||||
) : null}
|
||||
<p className="mt-1 text-[10.5px] tabular-nums text-muted-foreground">
|
||||
{formatElapsed(run.createdAt, elapsedEnd)} · {run.sources.length}{" "}
|
||||
{formatElapsed(run.createdAt, elapsedEnd)} · {sourceCount}{" "}
|
||||
sources ·{" "}
|
||||
{run.steps.filter((step) => step.status === "completed").length}{" "}
|
||||
actions
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ export function ResearchMessage(): ReactElement {
|
|||
markdown={run.report}
|
||||
className="max-h-none overflow-visible border-0 bg-transparent p-0 text-[15.5px]"
|
||||
/>
|
||||
<SourcesGroup sources={sources} />
|
||||
<SourcesGroup sources={sources} allowRemoteIcons={false} />
|
||||
<DocumentSourcesGroup sources={documentSources} />
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue