Fix hydrated tool call statuses and show tool outputs
This commit is contained in:
@@ -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 })}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user