fix: add null checks and type guards to all MessageAgentTools to prevent rendering errors (#11512)

* Initial plan

* fix: add null checks to BashTool to prevent rendering errors

Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com>

* fix: add null checks to all MessageAgentTools to prevent rendering errors

Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com>

* fix: add Array.isArray checks to prevent map errors on non-array values

Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com>

* fix: add typeof checks for string operations to prevent type errors

Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com>

* refactor: remove redundant typeof string checks for typed outputs

Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: DeJeune <67425183+DeJeune@users.noreply.github.com>
This commit is contained in:
Copilot 2025-11-28 10:12:21 +08:00 committed by GitHub
parent 77a9504f74
commit 7ce1590eaf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 55 additions and 51 deletions

View File

@ -76,7 +76,7 @@ export function BashOutputTool({
input, input,
output output
}: { }: {
input: BashOutputToolInput input?: BashOutputToolInput
output?: BashOutputToolOutput output?: BashOutputToolOutput
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
const parsedOutput = parseBashOutput(output) const parsedOutput = parseBashOutput(output)
@ -144,7 +144,7 @@ export function BashOutputTool({
label="Bash Output" label="Bash Output"
params={ params={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Tag className="py-0 font-mono text-xs">{input.bash_id}</Tag> <Tag className="py-0 font-mono text-xs">{input?.bash_id}</Tag>
{statusConfig && ( {statusConfig && (
<Tag <Tag
color={statusConfig.color} color={statusConfig.color}

View File

@ -11,14 +11,14 @@ export function BashTool({
input, input,
output output
}: { }: {
input: BashToolInputType input?: BashToolInputType
output?: BashToolOutputType output?: BashToolOutputType
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
// 如果有输出,计算输出行数 // 如果有输出,计算输出行数
const outputLines = output ? output.split('\n').length : 0 const outputLines = output ? output.split('\n').length : 0
// 处理命令字符串的截断 // 处理命令字符串的截断,添加空值检查
const command = input.command const command = input?.command ?? ''
const needsTruncate = command.length > MAX_TAG_LENGTH const needsTruncate = command.length > MAX_TAG_LENGTH
const displayCommand = needsTruncate ? `${command.slice(0, MAX_TAG_LENGTH)}...` : command const displayCommand = needsTruncate ? `${command.slice(0, MAX_TAG_LENGTH)}...` : command
@ -31,7 +31,7 @@ export function BashTool({
<ToolTitle <ToolTitle
icon={<Terminal className="h-4 w-4" />} icon={<Terminal className="h-4 w-4" />}
label="Bash" label="Bash"
params={input.description} params={input?.description}
stats={output ? `${outputLines} ${outputLines === 1 ? 'line' : 'lines'}` : undefined} stats={output ? `${outputLines} ${outputLines === 1 ? 'line' : 'lines'}` : undefined}
/> />
<div className="mt-1"> <div className="mt-1">

View File

@ -32,19 +32,19 @@ export function EditTool({
input, input,
output output
}: { }: {
input: EditToolInput input?: EditToolInput
output?: EditToolOutput output?: EditToolOutput
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
return { return {
key: AgentToolsType.Edit, key: AgentToolsType.Edit,
label: <ToolTitle icon={<FileEdit className="h-4 w-4" />} label="Edit" params={input.file_path} />, label: <ToolTitle icon={<FileEdit className="h-4 w-4" />} label="Edit" params={input?.file_path} />,
children: ( children: (
<> <>
{/* Diff View */} {/* Diff View */}
{/* Old Content */} {/* Old Content */}
{renderCodeBlock(input.old_string, 'old')} {renderCodeBlock(input?.old_string ?? '', 'old')}
{/* New Content */} {/* New Content */}
{renderCodeBlock(input.new_string, 'new')} {renderCodeBlock(input?.new_string ?? '', 'new')}
{/* Output */} {/* Output */}
{output} {output}
</> </>

View File

@ -10,18 +10,19 @@ export function ExitPlanModeTool({
input, input,
output output
}: { }: {
input: ExitPlanModeToolInput input?: ExitPlanModeToolInput
output?: ExitPlanModeToolOutput output?: ExitPlanModeToolOutput
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
const plan = input?.plan ?? ''
return { return {
key: AgentToolsType.ExitPlanMode, key: AgentToolsType.ExitPlanMode,
label: ( label: (
<ToolTitle <ToolTitle
icon={<DoorOpen className="h-4 w-4" />} icon={<DoorOpen className="h-4 w-4" />}
label="ExitPlanMode" label="ExitPlanMode"
stats={`${input.plan.split('\n\n').length} plans`} stats={`${plan.split('\n\n').length} plans`}
/> />
), ),
children: <ReactMarkdown>{input.plan + '\n\n' + (output ?? '')}</ReactMarkdown> children: <ReactMarkdown>{plan + '\n\n' + (output ?? '')}</ReactMarkdown>
} }
} }

View File

@ -8,7 +8,7 @@ export function GlobTool({
input, input,
output output
}: { }: {
input: GlobToolInputType input?: GlobToolInputType
output?: GlobToolOutputType output?: GlobToolOutputType
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
// 如果有输出,计算文件数量 // 如果有输出,计算文件数量
@ -20,7 +20,7 @@ export function GlobTool({
<ToolTitle <ToolTitle
icon={<FolderSearch className="h-4 w-4" />} icon={<FolderSearch className="h-4 w-4" />}
label="Glob" label="Glob"
params={input.pattern} params={input?.pattern}
stats={output ? `${lineCount} ${lineCount === 1 ? 'file' : 'files'}` : undefined} stats={output ? `${lineCount} ${lineCount === 1 ? 'file' : 'files'}` : undefined}
/> />
), ),

View File

@ -8,7 +8,7 @@ export function GrepTool({
input, input,
output output
}: { }: {
input: GrepToolInput input?: GrepToolInput
output?: GrepToolOutput output?: GrepToolOutput
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
// 如果有输出,计算结果行数 // 如果有输出,计算结果行数
@ -22,8 +22,8 @@ export function GrepTool({
label="Grep" label="Grep"
params={ params={
<> <>
{input.pattern} {input?.pattern}
{input.output_mode && <span className="ml-1">({input.output_mode})</span>} {input?.output_mode && <span className="ml-1">({input.output_mode})</span>}
</> </>
} }
stats={output ? `${resultLines} ${resultLines === 1 ? 'line' : 'lines'}` : undefined} stats={output ? `${resultLines} ${resultLines === 1 ? 'line' : 'lines'}` : undefined}

View File

@ -9,18 +9,19 @@ import { AgentToolsType } from './types'
export function MultiEditTool({ export function MultiEditTool({
input input
}: { }: {
input: MultiEditToolInput input?: MultiEditToolInput
output?: MultiEditToolOutput output?: MultiEditToolOutput
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
const edits = Array.isArray(input?.edits) ? input.edits : []
return { return {
key: AgentToolsType.MultiEdit, key: AgentToolsType.MultiEdit,
label: <ToolTitle icon={<FileText className="h-4 w-4" />} label="MultiEdit" params={input.file_path} />, label: <ToolTitle icon={<FileText className="h-4 w-4" />} label="MultiEdit" params={input?.file_path} />,
children: ( children: (
<div> <div>
{input.edits.map((edit, index) => ( {edits.map((edit, index) => (
<div key={index}> <div key={index}>
{renderCodeBlock(edit.old_string, 'old')} {renderCodeBlock(edit.old_string ?? '', 'old')}
{renderCodeBlock(edit.new_string, 'new')} {renderCodeBlock(edit.new_string ?? '', 'new')}
</div> </div>
))} ))}
</div> </div>

View File

@ -11,7 +11,7 @@ export function NotebookEditTool({
input, input,
output output
}: { }: {
input: NotebookEditToolInput input?: NotebookEditToolInput
output?: NotebookEditToolOutput output?: NotebookEditToolOutput
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
return { return {
@ -20,10 +20,10 @@ export function NotebookEditTool({
<> <>
<ToolTitle icon={<FileText className="h-4 w-4" />} label="NotebookEdit" /> <ToolTitle icon={<FileText className="h-4 w-4" />} label="NotebookEdit" />
<Tag className="mt-1" color="blue"> <Tag className="mt-1" color="blue">
{input.notebook_path}{' '} {input?.notebook_path}{' '}
</Tag> </Tag>
</> </>
), ),
children: <ReactMarkdown>{output}</ReactMarkdown> children: <ReactMarkdown>{output ?? ''}</ReactMarkdown>
} }
} }

View File

@ -46,7 +46,7 @@ export function ReadTool({
input, input,
output output
}: { }: {
input: ReadToolInputType input?: ReadToolInputType
output?: ReadToolOutputType output?: ReadToolOutputType
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
const outputString = normalizeOutputString(output) const outputString = normalizeOutputString(output)
@ -58,7 +58,7 @@ export function ReadTool({
<ToolTitle <ToolTitle
icon={<FileText className="h-4 w-4" />} icon={<FileText className="h-4 w-4" />}
label="Read File" label="Read File"
params={input.file_path.split('/').pop()} params={input?.file_path?.split('/').pop()}
stats={stats ? `${stats.lineCount} lines, ${stats.formatSize(stats.fileSize)}` : undefined} stats={stats ? `${stats.lineCount} lines, ${stats.formatSize(stats.fileSize)}` : undefined}
/> />
), ),

View File

@ -8,7 +8,7 @@ export function SearchTool({
input, input,
output output
}: { }: {
input: SearchToolInputType input?: SearchToolInputType
output?: SearchToolOutputType output?: SearchToolOutputType
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
// 如果有输出,计算结果数量 // 如果有输出,计算结果数量
@ -20,13 +20,13 @@ export function SearchTool({
<ToolTitle <ToolTitle
icon={<Search className="h-4 w-4" />} icon={<Search className="h-4 w-4" />}
label="Search" label="Search"
params={`"${input}"`} params={input ? `"${input}"` : undefined}
stats={output ? `${resultCount} ${resultCount === 1 ? 'result' : 'results'}` : undefined} stats={output ? `${resultCount} ${resultCount === 1 ? 'result' : 'results'}` : undefined}
/> />
), ),
children: ( children: (
<div> <div>
<StringInputTool input={input} label="Search Query" /> {input && <StringInputTool input={input} label="Search Query" />}
{output && ( {output && (
<div> <div>
<StringOutputTool output={output} label="Search Results" textColor="text-yellow-600 dark:text-yellow-400" /> <StringOutputTool output={output} label="Search Results" textColor="text-yellow-600 dark:text-yellow-400" />

View File

@ -8,12 +8,12 @@ export function SkillTool({
input, input,
output output
}: { }: {
input: SkillToolInput input?: SkillToolInput
output?: SkillToolOutput output?: SkillToolOutput
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
return { return {
key: 'tool', key: 'tool',
label: <ToolTitle icon={<PencilRuler className="h-4 w-4" />} label="Skill" params={input.command} />, label: <ToolTitle icon={<PencilRuler className="h-4 w-4" />} label="Skill" params={input?.command} />,
children: <div>{output}</div> children: <div>{output}</div>
} }
} }

View File

@ -9,19 +9,20 @@ export function TaskTool({
input, input,
output output
}: { }: {
input: TaskToolInputType input?: TaskToolInputType
output?: TaskToolOutputType output?: TaskToolOutputType
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
return { return {
key: 'tool', key: 'tool',
label: <ToolTitle icon={<Bot className="h-4 w-4" />} label="Task" params={input.description} />, label: <ToolTitle icon={<Bot className="h-4 w-4" />} label="Task" params={input?.description} />,
children: ( children: (
<div> <div>
{output?.map((item) => ( {Array.isArray(output) &&
<div key={item.type}> output.map((item) => (
<div>{item.type === 'text' ? <Markdown>{item.text}</Markdown> : item.text}</div> <div key={item.type}>
</div> <div>{item.type === 'text' ? <Markdown>{item.text}</Markdown> : item.text}</div>
))} </div>
))}
</div> </div>
) )
} }

View File

@ -38,9 +38,10 @@ const getStatusConfig = (status: TodoItem['status']) => {
export function TodoWriteTool({ export function TodoWriteTool({
input input
}: { }: {
input: TodoWriteToolInputType input?: TodoWriteToolInputType
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
const doneCount = input.todos.filter((todo) => todo.status === 'completed').length const todos = Array.isArray(input?.todos) ? input.todos : []
const doneCount = todos.filter((todo) => todo.status === 'completed').length
return { return {
key: AgentToolsType.TodoWrite, key: AgentToolsType.TodoWrite,
@ -49,12 +50,12 @@ export function TodoWriteTool({
icon={<ListTodo className="h-4 w-4" />} icon={<ListTodo className="h-4 w-4" />}
label="Todo Write" label="Todo Write"
params={`${doneCount} Done`} params={`${doneCount} Done`}
stats={`${input.todos.length} ${input.todos.length === 1 ? 'item' : 'items'}`} stats={`${todos.length} ${todos.length === 1 ? 'item' : 'items'}`}
/> />
), ),
children: ( children: (
<div className="space-y-3"> <div className="space-y-3">
{input.todos.map((todo, index) => { {todos.map((todo, index) => {
const statusConfig = getStatusConfig(todo.status) const statusConfig = getStatusConfig(todo.status)
return ( return (
<div key={index}> <div key={index}>

View File

@ -8,12 +8,12 @@ export function WebFetchTool({
input, input,
output output
}: { }: {
input: WebFetchToolInput input?: WebFetchToolInput
output?: WebFetchToolOutput output?: WebFetchToolOutput
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
return { return {
key: 'tool', key: 'tool',
label: <ToolTitle icon={<Globe className="h-4 w-4" />} label="Web Fetch" params={input.url} />, label: <ToolTitle icon={<Globe className="h-4 w-4" />} label="Web Fetch" params={input?.url} />,
children: <div>{output}</div> children: <div>{output}</div>
} }
} }

View File

@ -8,7 +8,7 @@ export function WebSearchTool({
input, input,
output output
}: { }: {
input: WebSearchToolInput input?: WebSearchToolInput
output?: WebSearchToolOutput output?: WebSearchToolOutput
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
// 如果有输出,计算结果数量 // 如果有输出,计算结果数量
@ -20,7 +20,7 @@ export function WebSearchTool({
<ToolTitle <ToolTitle
icon={<Globe className="h-4 w-4" />} icon={<Globe className="h-4 w-4" />}
label="Web Search" label="Web Search"
params={input.query} params={input?.query}
stats={output ? `${resultCount} ${resultCount === 1 ? 'result' : 'results'}` : undefined} stats={output ? `${resultCount} ${resultCount === 1 ? 'result' : 'results'}` : undefined}
/> />
), ),

View File

@ -7,12 +7,12 @@ import type { WriteToolInput, WriteToolOutput } from './types'
export function WriteTool({ export function WriteTool({
input input
}: { }: {
input: WriteToolInput input?: WriteToolInput
output?: WriteToolOutput output?: WriteToolOutput
}): NonNullable<CollapseProps['items']>[number] { }): NonNullable<CollapseProps['items']>[number] {
return { return {
key: 'tool', key: 'tool',
label: <ToolTitle icon={<FileText className="h-4 w-4" />} label="Write" params={input.file_path} />, label: <ToolTitle icon={<FileText className="h-4 w-4" />} label="Write" params={input?.file_path} />,
children: <div>{input.content}</div> children: <div>{input?.content}</div>
} }
} }