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
16 changed files with 55 additions and 51 deletions

View File

@@ -76,7 +76,7 @@ export function BashOutputTool({
input,
output
}: {
input: BashOutputToolInput
input?: BashOutputToolInput
output?: BashOutputToolOutput
}): NonNullable<CollapseProps['items']>[number] {
const parsedOutput = parseBashOutput(output)
@@ -144,7 +144,7 @@ export function BashOutputTool({
label="Bash Output"
params={
<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 && (
<Tag
color={statusConfig.color}

View File

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

View File

@@ -32,19 +32,19 @@ export function EditTool({
input,
output
}: {
input: EditToolInput
input?: EditToolInput
output?: EditToolOutput
}): NonNullable<CollapseProps['items']>[number] {
return {
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: (
<>
{/* Diff View */}
{/* Old Content */}
{renderCodeBlock(input.old_string, 'old')}
{renderCodeBlock(input?.old_string ?? '', 'old')}
{/* New Content */}
{renderCodeBlock(input.new_string, 'new')}
{renderCodeBlock(input?.new_string ?? '', 'new')}
{/* Output */}
{output}
</>

View File

@@ -10,18 +10,19 @@ export function ExitPlanModeTool({
input,
output
}: {
input: ExitPlanModeToolInput
input?: ExitPlanModeToolInput
output?: ExitPlanModeToolOutput
}): NonNullable<CollapseProps['items']>[number] {
const plan = input?.plan ?? ''
return {
key: AgentToolsType.ExitPlanMode,
label: (
<ToolTitle
icon={<DoorOpen className="h-4 w-4" />}
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,
output
}: {
input: GlobToolInputType
input?: GlobToolInputType
output?: GlobToolOutputType
}): NonNullable<CollapseProps['items']>[number] {
// 如果有输出,计算文件数量
@@ -20,7 +20,7 @@ export function GlobTool({
<ToolTitle
icon={<FolderSearch className="h-4 w-4" />}
label="Glob"
params={input.pattern}
params={input?.pattern}
stats={output ? `${lineCount} ${lineCount === 1 ? 'file' : 'files'}` : undefined}
/>
),

View File

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

View File

@@ -9,18 +9,19 @@ import { AgentToolsType } from './types'
export function MultiEditTool({
input
}: {
input: MultiEditToolInput
input?: MultiEditToolInput
output?: MultiEditToolOutput
}): NonNullable<CollapseProps['items']>[number] {
const edits = Array.isArray(input?.edits) ? input.edits : []
return {
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: (
<div>
{input.edits.map((edit, index) => (
{edits.map((edit, index) => (
<div key={index}>
{renderCodeBlock(edit.old_string, 'old')}
{renderCodeBlock(edit.new_string, 'new')}
{renderCodeBlock(edit.old_string ?? '', 'old')}
{renderCodeBlock(edit.new_string ?? '', 'new')}
</div>
))}
</div>

View File

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

View File

@@ -46,7 +46,7 @@ export function ReadTool({
input,
output
}: {
input: ReadToolInputType
input?: ReadToolInputType
output?: ReadToolOutputType
}): NonNullable<CollapseProps['items']>[number] {
const outputString = normalizeOutputString(output)
@@ -58,7 +58,7 @@ export function ReadTool({
<ToolTitle
icon={<FileText className="h-4 w-4" />}
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}
/>
),

View File

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

View File

@@ -8,12 +8,12 @@ export function SkillTool({
input,
output
}: {
input: SkillToolInput
input?: SkillToolInput
output?: SkillToolOutput
}): NonNullable<CollapseProps['items']>[number] {
return {
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>
}
}

View File

@@ -9,19 +9,20 @@ export function TaskTool({
input,
output
}: {
input: TaskToolInputType
input?: TaskToolInputType
output?: TaskToolOutputType
}): NonNullable<CollapseProps['items']>[number] {
return {
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: (
<div>
{output?.map((item) => (
<div key={item.type}>
<div>{item.type === 'text' ? <Markdown>{item.text}</Markdown> : item.text}</div>
</div>
))}
{Array.isArray(output) &&
output.map((item) => (
<div key={item.type}>
<div>{item.type === 'text' ? <Markdown>{item.text}</Markdown> : item.text}</div>
</div>
))}
</div>
)
}

View File

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

View File

@@ -8,12 +8,12 @@ export function WebFetchTool({
input,
output
}: {
input: WebFetchToolInput
input?: WebFetchToolInput
output?: WebFetchToolOutput
}): NonNullable<CollapseProps['items']>[number] {
return {
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>
}
}

View File

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

View File

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