feat: add navigable JSON summaries for tool output
Tool JSON output now starts with a compact navigable summary view. Expandable tool output includes quick open-file and diff actions for changed files. Reasoning headers strip stray HTML comments, and navigation tools stay compact.
This commit is contained in:
@@ -40,6 +40,8 @@ const EXPANDED_CONTENT_TRANSITION = { duration: 0.2, ease: 'easeOut' as const };
|
||||
/** Strip common markdown syntax so the header preview reads as plain text. */
|
||||
const stripMarkdown = (text: string): string =>
|
||||
text
|
||||
// Empty HTML comments are frequently appended by model tool wrappers.
|
||||
.replace(/<!--\s*-->/g, '')
|
||||
// Fenced code blocks → keep inner text on one line
|
||||
.replace(/```[\w]*\n?([\s\S]*?)```/g, (_, inner: string) => inner.trim())
|
||||
// Inline code
|
||||
@@ -476,84 +478,4 @@ const ReasoningPart = React.memo(({
|
||||
);
|
||||
});
|
||||
|
||||
type MergedReasoningPartProps = {
|
||||
parts: Part[];
|
||||
onContentChange?: (reason?: ContentChangeReason) => void;
|
||||
messageId: string;
|
||||
streamPhase?: StreamPhase;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders ALL reasoning parts for a message as a single collapsible block,
|
||||
* merging their text and spanning their combined time range.
|
||||
* This matches the VSCode Copilot pattern of showing one "Thought" block per turn.
|
||||
*/
|
||||
export const MergedReasoningPart = React.memo(({
|
||||
parts,
|
||||
onContentChange,
|
||||
messageId,
|
||||
streamPhase,
|
||||
}: MergedReasoningPartProps) => {
|
||||
const chatRenderMode = useUIStore((state) => state.chatRenderMode);
|
||||
|
||||
const mergedText = React.useMemo(() => {
|
||||
return parts
|
||||
.map((part) => {
|
||||
const p = part as PartWithText;
|
||||
return cleanReasoningText(p.text || p.content || '');
|
||||
})
|
||||
.filter((t) => t.length > 0)
|
||||
.join('\n\n');
|
||||
}, [parts]);
|
||||
|
||||
const mergedTime = React.useMemo(() => {
|
||||
let earliestStart: number | undefined;
|
||||
let latestEnd: number | undefined;
|
||||
|
||||
for (const part of parts) {
|
||||
const time = (part as PartWithText).time;
|
||||
if (typeof time?.start === 'number' && Number.isFinite(time.start)) {
|
||||
if (earliestStart === undefined || time.start < earliestStart) {
|
||||
earliestStart = time.start;
|
||||
}
|
||||
}
|
||||
if (typeof time?.end === 'number' && Number.isFinite(time.end)) {
|
||||
if (latestEnd === undefined || time.end > latestEnd) {
|
||||
latestEnd = time.end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return earliestStart !== undefined ? { start: earliestStart, end: latestEnd } : undefined;
|
||||
}, [parts]);
|
||||
|
||||
const canBeStreaming = streamPhase === undefined || streamPhase !== 'completed';
|
||||
const isStreaming = chatRenderMode === 'live' && canBeStreaming && parts.some(
|
||||
(part) => typeof (part as PartWithText).time?.end !== 'number',
|
||||
);
|
||||
|
||||
const throttledMergedText = useStreamingTextThrottle({
|
||||
text: mergedText,
|
||||
isStreaming,
|
||||
identityKey: `${messageId}:reasoning-merged`,
|
||||
});
|
||||
|
||||
const blockId = parts[0]?.id ?? `${messageId}-reasoning-merged`;
|
||||
|
||||
if (!throttledMergedText.trim()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<ReasoningTimelineBlock
|
||||
text={throttledMergedText}
|
||||
variant="thinking"
|
||||
onContentChange={onContentChange}
|
||||
blockId={blockId}
|
||||
time={mergedTime}
|
||||
isStreaming={isStreaming}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export default ReasoningPart;
|
||||
|
||||
Reference in New Issue
Block a user