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:
@@ -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}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -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>
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user