Improve the formatting of tool calls, show thinking, treat Reasoning and Thinking as the same thing (sorry Kant) (#7626)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jh-block
2026-03-17 14:36:28 +01:00
committed by GitHub
parent 88a89a83e3
commit a762fe1000
21 changed files with 262 additions and 329 deletions
File diff suppressed because one or more lines are too long
-6
View File
@@ -670,8 +670,6 @@ export type MessageContent = (TextContent & {
type: 'redactedThinking';
}) | (SystemNotificationContent & {
type: 'systemNotification';
}) | (ReasoningContent & {
type: 'reasoning';
});
export type MessageEvent = {
@@ -1009,10 +1007,6 @@ export type ReadResourceResponse = {
uri: string;
};
export type ReasoningContent = {
text: string;
};
export type Recipe = {
activities?: Array<string> | null;
author?: Author | null;
+9 -19
View File
@@ -5,7 +5,7 @@ import MarkdownContent from './MarkdownContent';
import ToolCallWithResponse from './ToolCallWithResponse';
import {
getTextAndImageContent,
getReasoningContent,
getThinkingContent,
getToolRequests,
getToolResponses,
getToolConfirmationContent,
@@ -48,7 +48,7 @@ export default function GooseMessage({
const contentRef = useRef<HTMLDivElement | null>(null);
let { textContent, imagePaths } = getTextAndImageContent(message);
const reasoningContent = getReasoningContent(message);
const thinkingContent = getThinkingContent(message);
const splitChainOfThought = (text: string): { displayText: string; cotText: string | null } => {
const regex = /<think>([\s\S]*?)<\/think>/i;
@@ -131,26 +131,16 @@ export default function GooseMessage({
return (
<div className="goose-message flex w-[90%] justify-start min-w-0">
<div className="flex flex-col w-full min-w-0">
{reasoningContent && (
<details className="mb-2">
<summary className="cursor-pointer text-xs text-textSubtle select-none">
Show reasoning
</summary>
<div className="mt-2 text-sm">
<MarkdownContent content={reasoningContent} />
</div>
</details>
{thinkingContent && (
<div className="mb-2 text-xs text-gray-400/70 italic">
<MarkdownContent content={thinkingContent} />
</div>
)}
{cotText && (
<details className="bg-background-secondary border border-border-primary rounded p-2 mb-2">
<summary className="cursor-pointer text-sm text-text-secondary select-none">
Show thinking
</summary>
<div className="mt-2">
<MarkdownContent content={cotText} />
</div>
</details>
<div className="mb-2 text-sm text-gray-400 italic">
<MarkdownContent content={cotText} />
</div>
)}
{(displayText.trim() || imagePaths.length > 0) && (
+33 -59
View File
@@ -1,5 +1,4 @@
import { useState } from 'react';
import MarkdownContent from './MarkdownContent';
import Expand from './ui/Expand';
export type ToolCallArgumentValue =
@@ -14,6 +13,12 @@ interface ToolCallArgumentsProps {
args: Record<string, ToolCallArgumentValue>;
}
function formatValue(value: ToolCallArgumentValue): string {
if (typeof value === 'string') return value;
if (typeof value === 'object' && value !== null) return JSON.stringify(value, null, 2);
return String(value);
}
export function ToolCallArguments({ args }: ToolCallArgumentsProps) {
const [expandedKeys, setExpandedKeys] = useState<Record<string, boolean>>({});
@@ -22,46 +27,33 @@ export function ToolCallArguments({ args }: ToolCallArgumentsProps) {
};
const renderValue = (key: string, value: ToolCallArgumentValue) => {
if (typeof value === 'string') {
const needsExpansion = value.length > 60;
const isExpanded = expandedKeys[key];
const text = formatValue(value).trim();
const needsExpansion = text.length > 60 || text.includes('\n');
const isExpanded = expandedKeys[key];
if (!needsExpansion) {
return (
<div className="font-sans text-sm mb-2">
<div className="flex flex-row">
<span className="text-text-secondary min-w-[140px]">{key}</span>
<span className="text-text-secondary">{value}</span>
</div>
</div>
);
}
return (
<div className={`font-sans text-sm mb-2 ${isExpanded ? '' : 'truncate min-w-0'}`}>
<div className={`flex flex-row items-stretch ${isExpanded ? '' : 'truncate min-w-0'}`}>
<button
onClick={() => toggleKey(key)}
className="flex text-left text-text-secondary min-w-[140px]"
>
<span>{key}</span>
</button>
<div className={`w-full flex items-stretch ${isExpanded ? '' : 'truncate min-w-0'}`}>
{isExpanded ? (
<div>
<MarkdownContent
content={value}
className="font-sans text-sm text-text-secondary"
/>
</div>
) : (
<button
onClick={() => toggleKey(key)}
className={`text-left text-text-secondary ${isExpanded ? '' : 'truncate min-w-0'}`}
>
{value}
</button>
)}
return (
<div className="font-sans text-sm mb-2">
<div className={`flex flex-row items-stretch ${!isExpanded && needsExpansion ? 'truncate min-w-0' : ''}`}>
<button
onClick={() => needsExpansion && toggleKey(key)}
className={`flex text-left text-text-secondary min-w-[140px] ${needsExpansion ? 'cursor-pointer' : 'cursor-default'}`}
>
<span>{key}</span>
</button>
<div className={`w-full flex items-stretch ${!isExpanded && needsExpansion ? 'truncate min-w-0' : ''}`}>
{isExpanded ? (
<pre className="font-mono text-xs text-text-secondary whitespace-pre-wrap max-w-full overflow-x-auto">
{text}
</pre>
) : (
<button
onClick={() => needsExpansion && toggleKey(key)}
className={`text-left text-text-secondary font-mono text-xs ${needsExpansion ? 'truncate min-w-0 cursor-pointer' : 'cursor-default'}`}
>
{text.split('\n')[0]}
</button>
)}
{needsExpansion && (
<button
onClick={() => toggleKey(key)}
className="flex flex-row items-stretch grow text-text-secondary pr-2"
@@ -69,27 +61,9 @@ export function ToolCallArguments({ args }: ToolCallArgumentsProps) {
<div className="min-w-2 grow" />
<Expand size={5} isExpanded={isExpanded} />
</button>
</div>
)}
</div>
</div>
);
}
// Handle non-string values (arrays, objects, etc.)
const content = Array.isArray(value)
? value.map((item, index) => `${index + 1}. ${JSON.stringify(item)}`).join('\n')
: typeof value === 'object' && value !== null
? JSON.stringify(value, null, 2)
: String(value);
return (
<div className="mb-2">
<div className="flex flex-row font-sans text-sm">
<span className="text-text-secondary min-w-[140px]">{key}</span>
<pre className="whitespace-pre-wrap text-text-secondary overflow-x-auto max-w-full">
{content}
</pre>
</div>
</div>
);
};
@@ -866,7 +866,7 @@ interface ToolResultViewProps {
isStartExpanded: boolean;
}
function ToolResultView({ toolCall, result, isStartExpanded }: ToolResultViewProps) {
function ToolResultView({ result, isStartExpanded }: ToolResultViewProps) {
const hasText = (c: ContentBlock): c is ContentBlock & { text: string } =>
'text' in c && typeof (c as Record<string, unknown>).text === 'string';
@@ -879,18 +879,6 @@ function ToolResultView({ toolCall, result, isStartExpanded }: ToolResultViewPro
const hasResource = (c: ContentBlock): c is ContentBlock & { resource: unknown } =>
'resource' in c;
const wrapMarkdown = (text: string): string => {
if (
['code_execution__list_functions', 'code_execution__get_function_details'].includes(
toolCall.name
)
) {
return '```typescript\n' + text + '\n```';
} else {
return text;
}
};
return (
<ToolCallExpandable
label={<span className="pl-4 py-1 font-sans text-sm">Output</span>}
@@ -898,10 +886,9 @@ function ToolResultView({ toolCall, result, isStartExpanded }: ToolResultViewPro
>
<div className="pl-4 pr-4 py-4">
{hasText(result) && (
<MarkdownContent
content={wrapMarkdown(result.text)}
className="whitespace-pre-wrap max-w-full overflow-x-auto"
/>
<pre className="font-mono text-xs whitespace-pre-wrap max-w-full overflow-x-auto">
{result.text.trim()}
</pre>
)}
{hasImage(result) && (
<img
@@ -8,7 +8,7 @@ import ToolCallWithResponse from '../ToolCallWithResponse';
import ImagePreview from '../ImagePreview';
import {
getTextAndImageContent,
getReasoningContent,
getThinkingContent,
ToolRequestMessageContent,
ToolResponseMessageContent,
} from '../../types/message';
@@ -83,7 +83,7 @@ export const SessionMessages: React.FC<SessionMessagesProps> = ({
messages
.map((message, index) => {
const { textContent, imagePaths } = getTextAndImageContent(message);
const reasoningContent = getReasoningContent(message);
const thinkingContent = getThinkingContent(message);
// Get tool requests from the message
const toolRequests = message.content
@@ -121,16 +121,11 @@ export const SessionMessages: React.FC<SessionMessagesProps> = ({
</div>
<div className="flex flex-col w-full">
{/* Reasoning content */}
{reasoningContent && (
<details className="mb-2">
<summary className="cursor-pointer text-xs text-textSubtle select-none">
Show reasoning
</summary>
<div className="mt-2 text-sm">
<MarkdownContent content={reasoningContent} />
</div>
</details>
{/* Thinking content */}
{thinkingContent && (
<div className="mb-2 text-sm text-gray-400 italic">
<MarkdownContent content={thinkingContent} />
</div>
)}
{/* Text content */}
+15
View File
@@ -187,6 +187,21 @@ function pushMessage(currentMessages: Message[], incomingMsg: Message): Message[
incomingMsg.content.length === 1
) {
lastContent.text += newContent.text;
} else if (
lastContent?.type === 'thinking' &&
newContent?.type === 'thinking' &&
incomingMsg.content.length === 1 &&
'thinking' in lastContent &&
'thinking' in newContent
) {
// For thinking blocks: if the new block has a signature, it's the complete
// block from content_block_stop — replace entirely. Otherwise append the delta.
if ('signature' in newContent && newContent.signature) {
lastContent.thinking = newContent.thinking;
lastContent.signature = newContent.signature;
} else {
lastContent.thinking += newContent.thinking;
}
} else {
lastMsg.content.push(...incomingMsg.content);
}
+5 -5
View File
@@ -97,16 +97,16 @@ export function getTextAndImageContent(message: Message): {
return { textContent, imagePaths };
}
export function getReasoningContent(message: Message): string | null {
const reasoningContents = message.content
.filter((content) => content.type === 'reasoning')
export function getThinkingContent(message: Message): string | null {
const thinkingContents = message.content
.filter((content) => content.type === 'thinking')
.map((content) => {
if ('text' in content) return content.text;
if ('thinking' in content) return content.thinking;
return '';
})
.filter((text) => text.length > 0);
return reasoningContents.length > 0 ? reasoningContents.join('') : null;
return thinkingContents.length > 0 ? thinkingContents.join('') : null;
}
export function getToolRequests(message: Message): (ToolRequest & { type: 'toolRequest' })[] {