fix(web): hide raw internals in Agent tool card (#481)

* fix(web): hide raw internals in Agent tool card (#480)

The Agent tool card was exposing raw JSON input (including full prompts)
and internal system messages (agentId, output_file paths, system
instructions) in the details dialog. Register dedicated views:

- knownTools: show description as title, subagent_type as subtitle
- AgentFullView: show description, type, background status (not prompt)
- AgentResultView: detect internal launch messages and show "Agent
  launched" instead; render actual results as markdown for completed
  agents

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* fix: widen Agent result redaction to catch more internal metadata variants

Use || instead of && so any single internal marker (agentId:,
output_file:, internal ID) triggers redaction, not just the combination
of all markers.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* fix: use structural checks for Agent metadata redaction

Replace loose substring matching (which could false-positive on
legitimate agent output) with:
1. Structural check: result object has agentId/output_file keys
2. Strict text pattern: starts with the exact launch message prefix

Also make the label state-aware: "Done" for completed, "Agent launched"
for running.

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

---------

Co-authored-by: HAPI <noreply@hapi.run>
This commit is contained in:
Haoqing Wang
2026-04-17 11:14:30 +08:00
committed by GitHub
co-authored by HAPI
parent 5d1e616585
commit 9248b6825f
3 changed files with 75 additions and 0 deletions
@@ -285,6 +285,18 @@ export const knownTools: Record<string, {
},
minimal: true
},
Agent: {
icon: () => <RocketIcon className={DEFAULT_ICON_CLASS} />,
title: (opts) => {
const description = getInputStringAny(opts.input, ['description'])
return description ?? 'Agent'
},
subtitle: (opts) => {
const model = getInputStringAny(opts.input, ['subagent_type'])
return model ?? null
},
minimal: true
},
CodexReasoning: {
icon: () => <BulbIcon className={DEFAULT_ICON_CLASS} />,
title: (opts) => getInputStringAny(opts.input, ['title']) ?? 'Reasoning',
@@ -11,6 +11,7 @@ import { MultiEditFullView, MultiEditView } from '@/components/ToolCard/views/Mu
import { TodoWriteView } from '@/components/ToolCard/views/TodoWriteView'
import { UpdatePlanView } from '@/components/ToolCard/views/UpdatePlanView'
import { WriteView } from '@/components/ToolCard/views/WriteView'
import { isObject } from '@hapi/protocol'
import { getInputStringAny } from '@/lib/toolInputUtils'
export type ToolViewProps = {
@@ -29,6 +30,25 @@ const SkillFullView: ToolViewComponent = ({ block }: ToolViewProps) => {
)
}
const AgentFullView: ToolViewComponent = ({ block }: ToolViewProps) => {
const input = block.tool.input
const description = getInputStringAny(input, ['description'])
const subagentType = getInputStringAny(input, ['subagent_type'])
const runInBackground = isObject(input) && input.run_in_background === true
return (
<div className="flex flex-col gap-1 text-sm">
{description && (
<div className="text-[var(--app-fg)]">{description}</div>
)}
<div className="flex gap-3 text-[var(--app-hint)]">
{subagentType && <span>Type: {subagentType}</span>}
{runInBackground && <span>Background</span>}
</div>
</div>
)
}
export const toolViewRegistry: Record<string, ToolViewComponent> = {
Edit: EditView,
MultiEdit: MultiEditView,
@@ -50,6 +70,7 @@ export const toolFullViewRegistry: Record<string, ToolViewComponent> = {
CodexDiff: CodexDiffFullView,
CodexPatch: CodexPatchView,
Skill: SkillFullView,
Agent: AgentFullView,
AskUserQuestion: AskUserQuestionView,
ExitPlanMode: ExitPlanModeView,
ask_user_question: AskUserQuestionView,
@@ -507,6 +507,47 @@ const TodoWriteResultView: ToolViewComponent = (props: ToolViewProps) => {
return <ChecklistList items={todos} />
}
const AgentResultView: ToolViewComponent = (props: ToolViewProps) => {
const { state, result } = props.block.tool
if (result === undefined || result === null) {
return <div className="text-sm text-[var(--app-hint)]">{placeholderForState(state)}</div>
}
// For errors, show the error text
if (state === 'error') {
const text = extractTextFromResult(result)
return (
<div className="text-sm text-red-600">
{text?.trim() ? text : 'Agent failed'}
</div>
)
}
const text = extractTextFromResult(result)
if (!text) {
return <div className="text-sm text-[var(--app-hint)]">{state === 'completed' ? 'Done' : placeholderForState(state)}</div>
}
// Detect internal launch metadata. Check structurally first (result object
// may carry agentId/output_file keys), then fall back to a strict text
// pattern that is unlikely to appear in normal agent prose.
const isInternalMeta = isObject(result) && ('agentId' in result || 'output_file' in result)
|| (text.startsWith('Async agent launched successfully.') && text.includes('agentId:'))
if (isInternalMeta) {
const label = state === 'completed' ? 'Done' : 'Agent launched'
return <div className="text-sm text-[var(--app-hint)]">{label}</div>
}
return (
<>
<MarkdownRenderer content={text} />
<RawJsonDevOnly value={result} />
</>
)
}
const SkillResultView: ToolViewComponent = (props: ToolViewProps) => {
const { state, result, input } = props.block.tool
@@ -597,6 +638,7 @@ export const toolResultViewRegistry: Record<string, ToolViewComponent> = {
CodexPatch: CodexPatchResultView,
CodexDiff: CodexDiffResultView,
Skill: SkillResultView,
Agent: AgentResultView,
AskUserQuestion: AskUserQuestionResultView,
ExitPlanMode: MarkdownResultView,
ask_user_question: AskUserQuestionResultView,