Fix hydrated tool call statuses and show tool outputs

This commit is contained in:
Mohamed Boudra
2025-11-15 13:29:55 +01:00
parent b5daa1fa66
commit bd77d53e02
5 changed files with 450 additions and 17 deletions
@@ -249,6 +249,8 @@ export function AgentStreamView({
toolName={toolLabel}
kind={data.kind}
args={data.raw}
result={data.result}
error={data.error}
status={data.status as "executing" | "completed" | "failed"}
onOpenDetails={() => handleOpenToolCallDetails({ payload })}
/>
+164
View File
@@ -26,6 +26,12 @@ import { baseColors, theme } from "@/styles/theme";
import { Colors } from "@/constants/theme";
import * as Clipboard from "expo-clipboard";
import type { TodoEntry } from "@/types/stream";
import {
extractCommandDetails,
extractEditEntries,
extractReadEntries,
} from "@/utils/tool-call-parsers";
import { DiffViewer } from "./diff-viewer";
interface UserMessageProps {
message: string;
@@ -960,6 +966,26 @@ const toolKindIcons: Record<string, any> = {
// Add more mappings as needed
};
function formatPreviewValue(value: unknown, limit = 800): string {
if (value === undefined || value === null) {
return "";
}
let text: string;
if (typeof value === "string") {
text = value.trim();
} else {
try {
text = JSON.stringify(value, null, 2);
} catch {
text = String(value);
}
}
if (text.length <= limit) {
return text;
}
return `${text.slice(0, limit)}…`;
}
export const ToolCall = memo(function ToolCall({
toolName,
kind,
@@ -969,6 +995,22 @@ export const ToolCall = memo(function ToolCall({
status,
onOpenDetails,
}: ToolCallProps) {
const editEntries = useMemo(() => extractEditEntries(args, result), [args, result]);
const readEntries = useMemo(() => extractReadEntries(result, args), [args, result]);
const commandDetails = useMemo(() => extractCommandDetails(args, result), [args, result]);
const primaryEditEntry = editEntries[0];
const primaryReadEntry = readEntries[0];
const genericResult =
result !== undefined &&
!commandDetails?.output &&
!primaryReadEntry &&
!primaryEditEntry
? formatPreviewValue(result)
: null;
const formattedError =
error !== undefined ? formatPreviewValue(error ?? null, 600) : null;
const spinAnim = useRef(new Animated.Value(0)).current;
useEffect(() => {
@@ -1055,6 +1097,128 @@ export const ToolCall = memo(function ToolCall({
{toolName}
</Text>
</View>
{(commandDetails ||
primaryEditEntry ||
primaryReadEntry ||
genericResult ||
formattedError) && (
<View style={toolCallStylesheet.expandedContent}>
{commandDetails &&
(commandDetails.command ||
commandDetails.cwd ||
commandDetails.exitCode !== undefined ||
commandDetails.output) && (
<View style={toolCallStylesheet.section}>
<Text style={toolCallStylesheet.sectionTitle}>Command</Text>
<View style={toolCallStylesheet.sectionContent}>
{commandDetails.command && (
<Text
style={toolCallStylesheet.sectionText}
numberOfLines={2}
>
{commandDetails.command}
</Text>
)}
{commandDetails.cwd && (
<Text
style={toolCallStylesheet.sectionText}
numberOfLines={1}
>
{commandDetails.cwd}
</Text>
)}
{commandDetails.exitCode !== undefined && (
<Text style={toolCallStylesheet.sectionText}>
Exit code:{" "}
{commandDetails.exitCode === null
? "Unknown"
: commandDetails.exitCode}
</Text>
)}
{commandDetails.output && (
<Text
style={toolCallStylesheet.sectionText}
numberOfLines={6}
>
{formatPreviewValue(commandDetails.output)}
</Text>
)}
</View>
</View>
)}
{primaryReadEntry && (
<View style={toolCallStylesheet.section}>
<Text style={toolCallStylesheet.sectionTitle}>
{primaryReadEntry.filePath
? `Read: ${primaryReadEntry.filePath}`
: "Read Output"}
</Text>
<View style={toolCallStylesheet.sectionContent}>
<Text
style={toolCallStylesheet.sectionText}
numberOfLines={6}
>
{formatPreviewValue(primaryReadEntry.content)}
</Text>
</View>
</View>
)}
{primaryEditEntry && (
<View style={toolCallStylesheet.section}>
<Text style={toolCallStylesheet.sectionTitle}>
{primaryEditEntry.filePath
? `Diff: ${primaryEditEntry.filePath}`
: "Diff"}
</Text>
<View style={toolCallStylesheet.sectionContent}>
<DiffViewer diffLines={primaryEditEntry.diffLines} maxHeight={160} />
</View>
</View>
)}
{genericResult && (
<View style={toolCallStylesheet.section}>
<Text style={toolCallStylesheet.sectionTitle}>Result</Text>
<View style={toolCallStylesheet.sectionContent}>
<Text style={toolCallStylesheet.sectionText} numberOfLines={6}>
{genericResult}
</Text>
</View>
</View>
)}
{formattedError && (
<View style={toolCallStylesheet.section}>
<Text
style={[
toolCallStylesheet.sectionTitle,
toolCallStylesheet.errorSectionTitle,
]}
>
Error
</Text>
<View
style={[
toolCallStylesheet.sectionContent,
toolCallStylesheet.errorSectionContent,
]}
>
<Text
style={[
toolCallStylesheet.sectionText,
toolCallStylesheet.errorSectionTitle,
]}
numberOfLines={6}
>
{formattedError}
</Text>
</View>
</View>
)}
</View>
)}
</View>
</Pressable>
);
+131 -6
View File
@@ -221,7 +221,12 @@ function appendAgentToolCall(
data: AgentToolCallData,
timestamp: Date
): StreamItem[] {
const normalizedStatus = normalizeToolCallStatus(data.status);
const normalizedStatus = normalizeToolCallStatus(
data.status,
data.result,
data.error,
data.raw
);
const callId = data.callId ?? extractToolCallId(data.raw);
const payloadData: AgentToolCallData = {
@@ -293,20 +298,140 @@ function isPermissionToolCall(raw: unknown): boolean {
return candidate.server === "permission" || candidate.kind === "permission";
}
function normalizeToolCallStatus(status?: string): "executing" | "completed" | "failed" {
const FAILED_STATUS_PATTERN = /fail|error|deny|reject|cancel|abort|exception|refus/;
const COMPLETED_STATUS_PATTERN =
/complete|success|granted|applied|done|resolved|finish|succeed|ok/;
function normalizeStatusString(
status?: string | null
): "executing" | "completed" | "failed" | null {
if (!status) {
return "executing";
return null;
}
const normalized = status.toLowerCase();
if (/fail|error|deny|reject|cancel/.test(normalized)) {
const normalized = status.trim().toLowerCase();
if (!normalized) {
return null;
}
if (FAILED_STATUS_PATTERN.test(normalized)) {
return "failed";
}
if (/complete|success|granted|applied|done|resolved/.test(normalized)) {
if (COMPLETED_STATUS_PATTERN.test(normalized)) {
return "completed";
}
return "executing";
}
function hasValue(value: unknown): boolean {
return value !== undefined && value !== null;
}
function inferStatusFromRaw(raw: unknown): "completed" | "failed" | null {
if (!hasValue(raw)) {
return null;
}
const queue: unknown[] = Array.isArray(raw) ? [...raw] : [raw];
const visited = new Set<object>();
while (queue.length > 0) {
const candidate = queue.shift();
if (!candidate || typeof candidate !== "object") {
continue;
}
if (visited.has(candidate as object)) {
continue;
}
visited.add(candidate as object);
const record = candidate as Record<string, unknown>;
if (record.is_error === true) {
return "failed";
}
const statusValue = normalizeStatusString(
typeof record.status === "string" ? record.status : undefined
);
if (statusValue === "failed") {
return "failed";
}
if (statusValue === "completed") {
return "completed";
}
if ("error" in record && hasValue(record.error)) {
return "failed";
}
if (typeof record.stderr === "string" && record.stderr.length > 0) {
return "failed";
}
const typeValue =
typeof record.type === "string" ? record.type.toLowerCase() : "";
if (typeValue) {
if (FAILED_STATUS_PATTERN.test(typeValue)) {
return "failed";
}
if (/result|response|output|success/.test(typeValue)) {
return "completed";
}
}
const exitCode =
typeof record.exitCode === "number"
? record.exitCode
: typeof record.exit_code === "number"
? record.exit_code
: null;
if (exitCode !== null) {
return exitCode === 0 ? "completed" : "failed";
}
const successValue =
typeof record.success === "boolean" ? record.success : null;
if (successValue !== null) {
return successValue ? "completed" : "failed";
}
for (const value of Object.values(record)) {
if (typeof value === "object" && value !== null) {
queue.push(value);
}
}
}
return null;
}
function normalizeToolCallStatus(
status?: string,
result?: unknown,
error?: unknown,
raw?: unknown
): "executing" | "completed" | "failed" {
const normalizedFromStatus = normalizeStatusString(status);
if (normalizedFromStatus === "failed") {
return "failed";
}
if (normalizedFromStatus === "completed") {
return "completed";
}
if (hasValue(error)) {
return "failed";
}
if (hasValue(result)) {
return "completed";
}
const inferredFromRaw = inferStatusFromRaw(raw);
if (inferredFromRaw) {
return inferredFromRaw;
}
return normalizedFromStatus ?? "executing";
}
const TOOL_CALL_ID_KEYS = [
"toolCallId",
"tool_call_id",