Update files

This commit is contained in:
Mohamed Boudra
2026-02-08 22:37:52 +07:00
parent 8317b0d89b
commit 0176614467
36 changed files with 4098 additions and 5114 deletions
@@ -47,7 +47,6 @@ import type { Agent } from "@/contexts/session-context";
import { useSessionStore } from "@/stores/session-store";
import { useFileExplorerActions } from "@/hooks/use-file-explorer-actions";
import type { DaemonClient } from "@server/client/daemon-client";
import { parseToolCallDisplay } from "@/utils/tool-call-parsers";
import { ToolCallDetailsContent } from "./tool-call-details";
import { QuestionFormCard } from "./question-form-card";
import { ToolCallSheetProvider } from "./tool-call-sheet";
@@ -363,11 +362,11 @@ export function AgentStreamView({
return (
<ToolCall
toolName={data.name}
provider={data.provider}
args={data.input}
result={data.result}
error={data.error}
status={data.status as "executing" | "completed" | "failed"}
status={data.status}
detail={data.detail}
cwd={agent.cwd}
metadata={data.metadata}
isLastInSequence={isLastInSequence}
@@ -904,17 +903,6 @@ function PermissionRequestCard({
return undefined;
}, [request]);
const toolCallDisplay = useMemo(() => {
if (isPlanRequest) {
return null;
}
return parseToolCallDisplay({
name: request.name ?? "unknown",
provider: request.provider,
input: request.input,
});
}, [isPlanRequest, request.name, request.provider, request.input]);
const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]);
const markdownRules = useMemo(() => {
@@ -1133,8 +1121,12 @@ function PermissionRequestCard({
</View>
) : null}
{!isPlanRequest && toolCallDisplay ? (
<ToolCallDetailsContent detail={toolCallDisplay.detail} maxHeight={200} />
{!isPlanRequest ? (
<ToolCallDetailsContent
input={request.input ?? null}
output={null}
maxHeight={200}
/>
) : null}
<Text
+79 -33
View File
@@ -62,18 +62,22 @@ import {
useUnistyles,
UnistylesRuntime,
} from "react-native-unistyles";
import { baseColors, theme } from "@/styles/theme";
import { theme } from "@/styles/theme";
import {
createMarkdownStyles,
} from "@/styles/markdown-styles";
import { Colors, Fonts } from "@/constants/theme";
import * as Clipboard from "expo-clipboard";
import type { TodoEntry } from "@/types/stream";
import { parseToolCallDisplay, type ToolCallKind } from "@/utils/tool-call-parsers";
import type { ToolCallDetail } from "@server/server/agent/agent-sdk-types";
import {
buildToolCallDisplayModel,
formatToolCallError,
type ToolCallKind,
} from "@/utils/tool-call-display";
import { getNowMs, isPerfLoggingEnabled, perfLog } from "@/utils/perf";
import { parseInlinePathToken, type InlinePathTarget } from "@/utils/inline-path";
export type { InlinePathTarget } from "@/utils/inline-path";
import { resolveToolCallPreview } from "./tool-call-preview";
import { useToolCallSheet } from "./tool-call-sheet";
import { ToolCallDetailsContent } from "./tool-call-details";
@@ -106,6 +110,29 @@ function useDisableOuterSpacing(disableOuterSpacing: boolean | undefined) {
return disableOuterSpacing ?? contextValue;
}
function hexToRgba(hexColor: string, alpha: number) {
const clampedAlpha = Math.max(0, Math.min(1, alpha));
const hex = hexColor.replace("#", "");
const normalized =
hex.length === 3
? hex
.split("")
.map((char) => `${char}${char}`)
.join("")
: hex;
if (!/^[\da-fA-F]{6}$/.test(normalized)) {
return `rgba(255, 255, 255, ${clampedAlpha})`;
}
const intValue = Number.parseInt(normalized, 16);
const red = (intValue >> 16) & 255;
const green = (intValue >> 8) & 255;
const blue = intValue & 255;
return `rgba(${red}, ${green}, ${blue}, ${clampedAlpha})`;
}
const userMessageStylesheet = StyleSheet.create((theme) => ({
container: {
flexDirection: "row",
@@ -418,8 +445,9 @@ const expandableBadgeStylesheet = StyleSheet.create((theme) => ({
},
shimmerOverlay: {
position: "absolute",
top: 0,
bottom: 0,
top: 3,
bottom: 3,
borderRadius: 999,
},
}));
@@ -1026,6 +1054,14 @@ const ExpandableBadge = memo(function ExpandableBadge({
}, [isLoading]);
const shimmerBandWidth = 14;
const shimmerGradientColors = useMemo(
() => ({
edge: hexToRgba(theme.colors.foreground, 0),
soft: hexToRgba(theme.colors.foreground, 0.08),
core: hexToRgba(theme.colors.foreground, 0.24),
}),
[theme.colors.foreground]
);
const shimmerStyle = useAnimatedStyle(() => {
const travel = badgeWidth + shimmerBandWidth;
return {
@@ -1112,13 +1148,13 @@ const ExpandableBadge = memo(function ExpandableBadge({
x2="1"
y2="0"
>
<Stop offset="0" stopColor={baseColors.white} stopOpacity="0" />
<Stop offset="0.34" stopColor={baseColors.white} stopOpacity="0" />
<Stop offset="0.46" stopColor={baseColors.white} stopOpacity="0.12" />
<Stop offset="0.5" stopColor={baseColors.white} stopOpacity="0.34" />
<Stop offset="0.54" stopColor={baseColors.white} stopOpacity="0.12" />
<Stop offset="0.66" stopColor={baseColors.white} stopOpacity="0" />
<Stop offset="1" stopColor={baseColors.white} stopOpacity="0" />
<Stop offset="0" stopColor={shimmerGradientColors.edge} />
<Stop offset="0.38" stopColor={shimmerGradientColors.edge} />
<Stop offset="0.48" stopColor={shimmerGradientColors.soft} />
<Stop offset="0.5" stopColor={shimmerGradientColors.core} />
<Stop offset="0.52" stopColor={shimmerGradientColors.soft} />
<Stop offset="0.62" stopColor={shimmerGradientColors.edge} />
<Stop offset="1" stopColor={shimmerGradientColors.edge} />
</LinearGradient>
</Defs>
<Rect width="100%" height="100%" fill="url(#shimmerGrad)" />
@@ -1155,11 +1191,11 @@ const ExpandableBadge = memo(function ExpandableBadge({
interface ToolCallProps {
toolName: string;
provider?: string;
args: any;
result?: any;
error?: any;
status: "executing" | "completed" | "failed";
status: "executing" | "running" | "completed" | "failed" | "canceled";
detail?: ToolCallDetail;
cwd?: string;
metadata?: Record<string, unknown>;
isLastInSequence?: boolean;
@@ -1183,11 +1219,11 @@ const TOOL_CALL_COMMIT_THRESHOLD_MS = 16;
export const ToolCall = memo(function ToolCall({
toolName,
provider,
args,
result,
error,
status,
detail,
cwd,
metadata,
isLastInSequence = false,
@@ -1204,20 +1240,14 @@ export const ToolCall = memo(function ToolCall({
UnistylesRuntime.breakpoint === "xs" ||
UnistylesRuntime.breakpoint === "sm";
const displayInfo = useMemo(
() =>
parseToolCallDisplay({
name: toolName,
provider,
input: args,
output: result,
error,
metadata,
cwd,
}),
[toolName, provider, args, result, error, metadata, cwd]
const displayModel = useMemo(
() => buildToolCallDisplayModel({ name: toolName, detail, metadata, cwd }),
[toolName, detail, metadata, cwd]
);
const { kind, displayName, summary, detail, errorText } = displayInfo;
const displayName = displayModel.displayName;
const kind: ToolCallKind = displayModel.kind;
const summary = displayModel.summary;
const errorText = useMemo(() => formatToolCallError(error), [error]);
const IconComponent = toolKindIcons[kind] || Wrench;
// Check if there's any content to display
@@ -1229,11 +1259,19 @@ export const ToolCall = memo(function ToolCall({
toggleStartRef.current = getNowMs();
}
if (isMobile) {
openToolCall(displayInfo);
openToolCall({
kind,
displayName,
summary,
detail,
input: args,
output: result,
errorText,
});
} else {
setIsExpanded((prev) => !prev);
}
}, [isMobile, openToolCall, displayInfo]);
}, [isMobile, openToolCall, kind, displayName, summary, detail, args, result, errorText]);
useEffect(() => {
if (isMobile || !isPerfLoggingEnabled()) {
@@ -1292,8 +1330,16 @@ export const ToolCall = memo(function ToolCall({
// Render inline details for desktop
const renderDetails = useCallback(() => {
if (isMobile) return null;
return <ToolCallDetailsContent detail={detail} errorText={errorText} maxHeight={400} />;
}, [isMobile, detail, errorText]);
return (
<ToolCallDetailsContent
detail={detail}
input={args}
output={result}
errorText={errorText}
maxHeight={400}
/>
);
}, [isMobile, detail, args, result, errorText]);
return (
<ExpandableBadge
@@ -1310,7 +1356,7 @@ export const ToolCall = memo(function ToolCall({
? () => null
: undefined
}
isLoading={status === "executing"}
isLoading={status === "executing" || status === "running"}
isError={status === "failed"}
isLastInSequence={isLastInSequence}
disableOuterSpacing={disableOuterSpacing}
@@ -3,10 +3,10 @@ import { View, Text, Platform, ScrollView as RNScrollView } from "react-native";
import { ScrollView as GHScrollView } from "react-native-gesture-handler";
import { StyleSheet } from "react-native-unistyles";
import { Fonts } from "@/constants/theme";
import type { ToolCallDetail } from "@server/server/agent/agent-sdk-types";
import {
buildLineDiff,
parseUnifiedDiff,
type ToolCallDetail,
} from "@/utils/tool-call-parsers";
import { DiffViewer } from "./diff-viewer";
import { getCodeInsets } from "./code-insets";
@@ -16,34 +16,39 @@ const ScrollView = Platform.OS === "web" ? RNScrollView : GHScrollView;
// ---- Content Component ----
interface ToolCallDetailsContentProps {
detail: ToolCallDetail;
detail?: ToolCallDetail;
input?: unknown | null;
output?: unknown | null;
errorText?: string;
maxHeight?: number;
}
export function ToolCallDetailsContent({
detail,
input,
output,
errorText,
maxHeight = 300,
}: ToolCallDetailsContentProps) {
// Compute diff lines for edit type
const diffLines = useMemo(() => {
if (detail.type !== "edit") return undefined;
if (!detail || detail.type !== "edit") return undefined;
// Use pre-computed unified diff if available (e.g., from apply_patch)
if (detail.unifiedDiff) {
return parseUnifiedDiff(detail.unifiedDiff);
}
return buildLineDiff(detail.oldString, detail.newString);
return buildLineDiff(detail.oldString ?? "", detail.newString ?? "");
}, [detail]);
const sections: ReactNode[] = [];
const isFullBleed = detail.type === "edit" || detail.type === "shell";
const isFullBleed =
detail?.type === "edit" || detail?.type === "shell" || detail?.type === "write";
const codeBlockStyle = isFullBleed ? styles.fullBleedBlock : styles.diffContainer;
if (detail.type === "shell") {
if (detail?.type === "shell") {
const command = detail.command.replace(/\n+$/, "");
const output = detail.output.replace(/^\n+/, "");
const hasOutput = output.length > 0;
const commandOutput = (detail.output ?? "").replace(/^\n+/, "");
const hasOutput = commandOutput.length > 0;
sections.push(
<View key="shell" style={styles.section}>
<View style={codeBlockStyle}>
@@ -63,7 +68,7 @@ export function ToolCallDetailsContent({
<Text selectable style={styles.scrollText}>
<Text style={styles.shellPrompt}>$ </Text>
{command}
{hasOutput ? `\n\n${output}` : ""}
{hasOutput ? `\n\n${commandOutput}` : ""}
</Text>
</View>
</ScrollView>
@@ -71,7 +76,7 @@ export function ToolCallDetailsContent({
</View>
</View>
);
} else if (detail.type === "edit") {
} else if (detail?.type === "edit") {
sections.push(
<View key="edit" style={styles.section}>
{diffLines ? (
@@ -81,7 +86,28 @@ export function ToolCallDetailsContent({
) : null}
</View>
);
} else if (detail.type === "read") {
} else if (detail?.type === "write") {
sections.push(
<View key="write" style={styles.section}>
{detail.content ? (
<ScrollView
style={[styles.scrollArea, { maxHeight }]}
contentContainerStyle={styles.scrollContent}
nestedScrollEnabled
showsVerticalScrollIndicator={true}
>
<ScrollView
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator={true}
>
<Text selectable style={styles.scrollText}>{detail.content}</Text>
</ScrollView>
</ScrollView>
) : null}
</View>
);
} else if (detail?.type === "read") {
sections.push(
<View key="read" style={styles.section}>
{(detail.offset !== undefined || detail.limit !== undefined) ? (
@@ -109,68 +135,49 @@ export function ToolCallDetailsContent({
) : null}
</View>
);
} else if (detail.type === "thinking") {
// Thinking: display the content as plain text
} else if (detail?.type === "search") {
sections.push(
<View key="thinking" style={styles.section}>
<ScrollView
style={[styles.scrollArea, { maxHeight }]}
contentContainerStyle={styles.scrollContent}
nestedScrollEnabled
showsVerticalScrollIndicator={true}
>
<Text selectable style={styles.scrollText}>{detail.content}</Text>
</ScrollView>
<View key="search" style={styles.section}>
<Text selectable style={styles.scrollText}>{detail.query}</Text>
</View>
);
} else {
// Generic tool: show input/output as key-value pairs
if (detail.input.length > 0) {
sections.push(
<View key="input-header" style={styles.groupHeader}>
<Text style={styles.groupHeaderText}>Input</Text>
</View>
);
detail.input.forEach((pair, index) => {
sections.push(
<View key={`input-${index}-${pair.key}`} style={styles.section}>
<Text style={styles.sectionTitle}>{pair.key}</Text>
<ScrollView
horizontal
nestedScrollEnabled
style={styles.jsonScroll}
contentContainerStyle={styles.jsonContent}
showsHorizontalScrollIndicator={true}
>
<Text selectable style={styles.scrollText}>{pair.value}</Text>
</ScrollView>
</View>
);
});
}
const sectionsFromTopLevel = [
{ title: "Input", value: input },
{ title: "Output", value: output },
].filter((entry) => entry.value !== null && entry.value !== undefined);
if (detail.output.length > 0) {
for (const section of sectionsFromTopLevel) {
let value = "";
try {
value =
typeof section.value === "string"
? section.value
: JSON.stringify(section.value, null, 2);
} catch {
value = String(section.value);
}
if (!value.length) {
continue;
}
sections.push(
<View key="output-header" style={styles.groupHeader}>
<Text style={styles.groupHeaderText}>Output</Text>
<View key={`${section.title}-header`} style={styles.groupHeader}>
<Text style={styles.groupHeaderText}>{section.title}</Text>
</View>
);
sections.push(
<View key={`${section.title}-value`} style={styles.section}>
<ScrollView
horizontal
nestedScrollEnabled
style={styles.jsonScroll}
contentContainerStyle={styles.jsonContent}
showsHorizontalScrollIndicator={true}
>
<Text selectable style={styles.scrollText}>{value}</Text>
</ScrollView>
</View>
);
detail.output.forEach((pair, index) => {
sections.push(
<View key={`output-${index}-${pair.key}`} style={styles.section}>
<Text style={styles.sectionTitle}>{pair.key}</Text>
<ScrollView
horizontal
nestedScrollEnabled
style={styles.jsonScroll}
contentContainerStyle={styles.jsonContent}
showsHorizontalScrollIndicator={true}
>
<Text selectable style={styles.scrollText}>{pair.value}</Text>
</ScrollView>
</View>
);
});
}
}
@@ -1,74 +0,0 @@
import { describe, expect, it } from "vitest";
import { resolveToolCallPreview } from "./tool-call-preview";
import type { CommandDetails, EditEntry, ReadEntry } from "@/utils/tool-call-parsers";
describe("resolveToolCallPreview", () => {
it("prefers parsed hydration payloads when provided", () => {
const parsedEdits: EditEntry[] = [
{
filePath: "README.md",
diffLines: [
{ type: "header", content: "@@ -1 +1 @@" },
{ type: "remove", content: "-Old" },
{ type: "add", content: "+New" },
],
},
];
const parsedReads: ReadEntry[] = [
{
filePath: "README.md",
content: "# Hydrated\nFinal text\n",
},
];
const parsedCommand: CommandDetails = {
command: "ls",
cwd: "/tmp/hydrated",
output: "README.md\npackages\n",
exitCode: 0,
};
const preview = resolveToolCallPreview({
parsedEditEntries: parsedEdits,
parsedReadEntries: parsedReads,
parsedCommandDetails: parsedCommand,
});
expect(preview.editEntries).toBe(parsedEdits);
expect(preview.readEntries).toBe(parsedReads);
expect(preview.commandDetails).toBe(parsedCommand);
});
it("falls back to derived parser output when hydration metadata is missing", () => {
const args = {
type: "mcp_tool_use",
id: "call_fallback",
name: "apply_patch",
server: "editor",
input: {
file_path: "README.md",
patch: "*** Begin Patch\n*** Update File: README.md\n@@\n-Old\n+New\n*** End Patch",
},
};
const result = {
changes: [
{
file_path: "README.md",
previous_content: "Old\n",
content: "New\n",
},
],
};
const preview = resolveToolCallPreview({
args,
result,
});
expect(preview.editEntries[0]?.diffLines.length).toBeGreaterThan(0);
expect(
preview.editEntries[0]?.diffLines.some((line) => line.content.includes("+New"))
).toBe(true);
expect(preview.commandDetails).toBeNull();
});
});
@@ -1,40 +0,0 @@
import {
extractCommandDetails,
extractEditEntries,
extractReadEntries,
type CommandDetails,
type EditEntry,
type ReadEntry,
} from "@/utils/tool-call-parsers";
export type ToolCallPreviewSource = {
args?: unknown;
result?: unknown;
parsedEditEntries?: EditEntry[] | undefined;
parsedReadEntries?: ReadEntry[] | undefined;
parsedCommandDetails?: CommandDetails | null | undefined;
};
export type ToolCallPreview = {
editEntries: EditEntry[];
readEntries: ReadEntry[];
commandDetails: CommandDetails | null | undefined;
};
export function resolveToolCallPreview({
args,
result,
parsedEditEntries,
parsedReadEntries,
parsedCommandDetails,
}: ToolCallPreviewSource): ToolCallPreview {
const fallbackEditEntries = extractEditEntries(args, result);
const fallbackReadEntries = extractReadEntries(result, args);
const fallbackCommandDetails = extractCommandDetails(args, result);
return {
editEntries: parsedEditEntries ?? fallbackEditEntries,
readEntries: parsedReadEntries ?? fallbackReadEntries,
commandDetails: parsedCommandDetails ?? fallbackCommandDetails,
};
}
@@ -16,12 +16,21 @@ import {
BottomSheetBackgroundProps,
} from "@gorhom/bottom-sheet";
import { Pencil, Eye, SquareTerminal, Search, Bot, Wrench, X } from "lucide-react-native";
import type { ToolCallDisplayInfo } from "@/utils/tool-call-parsers";
import type { ToolCallDetail } from "@server/server/agent/agent-sdk-types";
import type { ToolCallKind } from "@/utils/tool-call-display";
import { ToolCallDetailsContent } from "./tool-call-details";
// ----- Types -----
export type ToolCallSheetData = ToolCallDisplayInfo;
export type ToolCallSheetData = {
kind: ToolCallKind;
displayName: string;
summary?: string;
detail?: ToolCallDetail;
input?: unknown | null;
output?: unknown | null;
errorText?: string;
};
interface ToolCallSheetContextValue {
openToolCall: (data: ToolCallSheetData) => void;
@@ -133,7 +142,7 @@ interface ToolCallSheetContentProps {
}
function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
const { kind, displayName, detail, errorText } = data;
const { kind, displayName, detail, input, output, errorText } = data;
const IconComponent = toolKindIcons[kind] || Wrench;
@@ -157,7 +166,12 @@ function ToolCallSheetContent({ data, onClose }: ToolCallSheetContentProps) {
style={styles.content}
contentContainerStyle={styles.contentContainer}
>
<ToolCallDetailsContent detail={detail} errorText={errorText} />
<ToolCallDetailsContent
detail={detail}
input={input}
output={output}
errorText={errorText}
/>
</BottomSheetScrollView>
</View>
);