* Extract client SDK package * Polish SDK client identity defaults * Build client before dependent CI jobs * Restore daemon client server export * Extract protocol and client SDK packages * Fix provider override schema validation * Fix app test daemon client imports * Simplify workspace build targets * Fix CLI test server build bootstrap * Run SDK package tests in CI * Fix rebase package split drift * Restore lockfile registry metadata * Update SDK config test for prompt default * Move terminal stream router test to client package * Fix rebase drift for protocol imports * Fix SDK agent capability fixture * Restore legacy server client exports * Fix server export compatibility test * Advertise custom mode icon client capability * Remove server daemon-client exports * Format rebased mode control import * Fix rebase drift for protocol imports Files added by upstream PRs (#893, #1147, #1154) referenced the pre-split shared/ paths that this branch moves into @getpaseo/protocol. Redirect those imports to the protocol package so typecheck stays green after the rebase.
935 lines
25 KiB
TypeScript
935 lines
25 KiB
TypeScript
import React, { useMemo, type ReactNode } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
ScrollView as RNScrollView,
|
|
type StyleProp,
|
|
type ViewStyle,
|
|
} 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 "@getpaseo/protocol/agent-types";
|
|
import { buildLineDiff, parseUnifiedDiff, type DiffLine } from "@/utils/tool-call-parsers";
|
|
import { hasMeaningfulToolCallDetail } from "@/utils/tool-call-detail-state";
|
|
import { useWebScrollbarStyle } from "@/hooks/use-web-scrollbar-style";
|
|
import { inlineUnistylesStyle } from "@/styles/unistyles-inline-style";
|
|
import { DiffViewer } from "./diff-viewer";
|
|
import { getCodeInsets } from "./code-insets";
|
|
import { isWeb } from "@/constants/platform";
|
|
|
|
const ScrollView = isWeb ? RNScrollView : GHScrollView;
|
|
|
|
// ---- Content Component ----
|
|
|
|
interface ToolCallDetailsContentProps {
|
|
detail?: ToolCallDetail;
|
|
errorText?: string;
|
|
maxHeight?: number;
|
|
fillAvailableHeight?: boolean;
|
|
showLoadingSkeleton?: boolean;
|
|
}
|
|
|
|
interface DetailStyles {
|
|
sectionFillStyle: StyleProp<ViewStyle>;
|
|
codeBlockFillStyle: StyleProp<ViewStyle>;
|
|
codeVerticalScrollStyle: StyleProp<ViewStyle>;
|
|
scrollAreaFillStyle: StyleProp<ViewStyle>;
|
|
scrollAreaStyle: StyleProp<ViewStyle>;
|
|
jsonScrollCombined: StyleProp<ViewStyle>;
|
|
jsonScrollErrorCombined: StyleProp<ViewStyle>;
|
|
fullBleedContainerStyle: StyleProp<ViewStyle>;
|
|
loadingContainerStyle: StyleProp<ViewStyle>;
|
|
webScrollbarStyle: StyleProp<ViewStyle>;
|
|
resolvedMaxHeight: number | undefined;
|
|
shouldFill: boolean;
|
|
isFullBleed: boolean;
|
|
}
|
|
|
|
function resolveIsFullBleed(detail: ToolCallDetail | undefined): boolean {
|
|
return detail?.type === "edit" || detail?.type === "shell" || detail?.type === "write";
|
|
}
|
|
|
|
function resolveShouldFill(
|
|
detail: ToolCallDetail | undefined,
|
|
fillAvailableHeight: boolean,
|
|
): boolean {
|
|
if (!fillAvailableHeight) return false;
|
|
const t = detail?.type;
|
|
return t === "shell" || t === "edit" || t === "write" || t === "read" || t === "sub_agent";
|
|
}
|
|
|
|
function useDetailStyles(
|
|
detail: ToolCallDetail | undefined,
|
|
resolvedMaxHeight: number | undefined,
|
|
fillAvailableHeight: boolean,
|
|
): DetailStyles {
|
|
const webScrollbarStyle = useWebScrollbarStyle();
|
|
const isFullBleed = resolveIsFullBleed(detail);
|
|
const shouldFill = resolveShouldFill(detail, fillAvailableHeight);
|
|
const codeBlockStyle = isFullBleed ? styles.fullBleedBlock : styles.diffContainer;
|
|
|
|
const sectionFillStyle = useMemo(
|
|
() => [styles.section, shouldFill && styles.fillHeight],
|
|
[shouldFill],
|
|
);
|
|
const codeBlockFillStyle = useMemo(
|
|
() => [codeBlockStyle, shouldFill && styles.fillHeight],
|
|
[codeBlockStyle, shouldFill],
|
|
);
|
|
const codeVerticalScrollStyle = useMemo(
|
|
() => [
|
|
styles.codeVerticalScroll,
|
|
resolvedMaxHeight !== undefined && inlineUnistylesStyle({ maxHeight: resolvedMaxHeight }),
|
|
shouldFill && styles.fillHeight,
|
|
webScrollbarStyle,
|
|
],
|
|
[resolvedMaxHeight, shouldFill, webScrollbarStyle],
|
|
);
|
|
const scrollAreaFillStyle = useMemo(
|
|
() => [
|
|
styles.scrollArea,
|
|
resolvedMaxHeight !== undefined && inlineUnistylesStyle({ maxHeight: resolvedMaxHeight }),
|
|
shouldFill && styles.fillHeight,
|
|
webScrollbarStyle,
|
|
],
|
|
[resolvedMaxHeight, shouldFill, webScrollbarStyle],
|
|
);
|
|
const scrollAreaStyle = useMemo(
|
|
() => [
|
|
styles.scrollArea,
|
|
resolvedMaxHeight !== undefined && inlineUnistylesStyle({ maxHeight: resolvedMaxHeight }),
|
|
webScrollbarStyle,
|
|
],
|
|
[resolvedMaxHeight, webScrollbarStyle],
|
|
);
|
|
const jsonScrollCombined = useMemo(
|
|
() => [styles.jsonScroll, webScrollbarStyle],
|
|
[webScrollbarStyle],
|
|
);
|
|
const jsonScrollErrorCombined = useMemo(
|
|
() => [styles.jsonScroll, styles.jsonScrollError, webScrollbarStyle],
|
|
[webScrollbarStyle],
|
|
);
|
|
const fullBleedContainerStyle = useMemo(
|
|
() => [
|
|
isFullBleed ? styles.fullBleedContainer : styles.paddedContainer,
|
|
shouldFill && styles.fillHeight,
|
|
],
|
|
[isFullBleed, shouldFill],
|
|
);
|
|
const loadingContainerStyle = useMemo(
|
|
() => [styles.loadingContainer, fillAvailableHeight && styles.fillHeight],
|
|
[fillAvailableHeight],
|
|
);
|
|
|
|
return {
|
|
sectionFillStyle,
|
|
codeBlockFillStyle,
|
|
codeVerticalScrollStyle,
|
|
scrollAreaFillStyle,
|
|
scrollAreaStyle,
|
|
jsonScrollCombined,
|
|
jsonScrollErrorCombined,
|
|
fullBleedContainerStyle,
|
|
loadingContainerStyle,
|
|
webScrollbarStyle,
|
|
resolvedMaxHeight,
|
|
shouldFill,
|
|
isFullBleed,
|
|
};
|
|
}
|
|
|
|
function useDiffLines(detail: ToolCallDetail | undefined): DiffLine[] | undefined {
|
|
return useMemo(() => {
|
|
if (!detail || detail.type !== "edit") return undefined;
|
|
if (detail.unifiedDiff) {
|
|
return parseUnifiedDiff(detail.unifiedDiff);
|
|
}
|
|
return buildLineDiff(detail.oldString ?? "", detail.newString ?? "");
|
|
}, [detail]);
|
|
}
|
|
|
|
interface ShellDetailProps {
|
|
command: string;
|
|
output: string | null | undefined;
|
|
ds: DetailStyles;
|
|
}
|
|
|
|
function ShellDetailSection({ command, output, ds }: ShellDetailProps) {
|
|
const normalizedCommand = command.replace(/\n+$/, "");
|
|
const commandOutput = (output ?? "").replace(/^\n+/, "");
|
|
const hasOutput = commandOutput.length > 0;
|
|
return (
|
|
<View style={ds.sectionFillStyle}>
|
|
<View style={ds.codeBlockFillStyle}>
|
|
<ScrollView
|
|
style={ds.codeVerticalScrollStyle}
|
|
contentContainerStyle={styles.codeVerticalContent}
|
|
nestedScrollEnabled
|
|
showsVerticalScrollIndicator
|
|
>
|
|
<ScrollView
|
|
horizontal
|
|
nestedScrollEnabled
|
|
showsHorizontalScrollIndicator
|
|
style={ds.webScrollbarStyle}
|
|
contentContainerStyle={styles.codeHorizontalContent}
|
|
>
|
|
<View style={styles.codeLine}>
|
|
<Text selectable style={styles.scrollText}>
|
|
<Text style={styles.shellPrompt}>$ </Text>
|
|
{normalizedCommand}
|
|
{hasOutput ? `\n\n${commandOutput}` : ""}
|
|
</Text>
|
|
</View>
|
|
</ScrollView>
|
|
</ScrollView>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
interface WorktreeSetupDetailProps {
|
|
log: string;
|
|
branchName: string;
|
|
worktreePath: string;
|
|
ds: DetailStyles;
|
|
}
|
|
|
|
function WorktreeSetupDetailSection({
|
|
log,
|
|
branchName,
|
|
worktreePath,
|
|
ds,
|
|
}: WorktreeSetupDetailProps) {
|
|
const setupLog = log.replace(/^\n+/, "");
|
|
const hasLog = setupLog.length > 0;
|
|
return (
|
|
<View style={ds.sectionFillStyle}>
|
|
<View style={ds.codeBlockFillStyle}>
|
|
<ScrollView
|
|
style={ds.codeVerticalScrollStyle}
|
|
contentContainerStyle={styles.codeVerticalContent}
|
|
nestedScrollEnabled
|
|
showsVerticalScrollIndicator
|
|
>
|
|
<ScrollView
|
|
horizontal
|
|
nestedScrollEnabled
|
|
showsHorizontalScrollIndicator
|
|
style={ds.webScrollbarStyle}
|
|
contentContainerStyle={styles.codeHorizontalContent}
|
|
>
|
|
<View style={styles.codeLine}>
|
|
<Text selectable style={styles.scrollText}>
|
|
{hasLog ? setupLog : `Preparing worktree ${branchName} at ${worktreePath}`}
|
|
</Text>
|
|
</View>
|
|
</ScrollView>
|
|
</ScrollView>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function resolveSubAgentFallbackHeader(
|
|
subAgentType: string | null | undefined,
|
|
description: string | null | undefined,
|
|
): string {
|
|
if (subAgentType && description) {
|
|
return `${subAgentType}: ${description}`;
|
|
}
|
|
return subAgentType ?? description ?? "Sub-agent activity";
|
|
}
|
|
|
|
interface SubAgentDetailProps {
|
|
log: string;
|
|
childSessionId: string | null | undefined;
|
|
subAgentType: string | null | undefined;
|
|
description: string | null | undefined;
|
|
ds: DetailStyles;
|
|
}
|
|
|
|
interface SubAgentActivityRow {
|
|
index: number;
|
|
toolName: string;
|
|
summary?: string;
|
|
}
|
|
|
|
interface ParsedSubAgentLog {
|
|
actions: SubAgentActivityRow[];
|
|
remainingLog: string;
|
|
}
|
|
|
|
function parseBracketedSubAgentLine(line: string, index: number): SubAgentActivityRow | null {
|
|
const match = line.match(/^\[([^\]]+)\](?:\s+(.*))?$/);
|
|
if (!match) {
|
|
return null;
|
|
}
|
|
const toolName = match[1]?.trim();
|
|
if (!toolName) {
|
|
return null;
|
|
}
|
|
const summary = match[2]?.trim();
|
|
return {
|
|
index,
|
|
toolName,
|
|
...(summary ? { summary } : {}),
|
|
};
|
|
}
|
|
|
|
function parseSubAgentLog(log: string): ParsedSubAgentLog {
|
|
const actions: SubAgentActivityRow[] = [];
|
|
const remainingLines: string[] = [];
|
|
for (const line of log.replace(/^\n+/, "").split("\n")) {
|
|
const normalizedLine = line.trim();
|
|
if (!normalizedLine) {
|
|
continue;
|
|
}
|
|
const parsedAction = parseBracketedSubAgentLine(normalizedLine, actions.length + 1);
|
|
if (parsedAction) {
|
|
actions.push(parsedAction);
|
|
} else {
|
|
remainingLines.push(line);
|
|
}
|
|
}
|
|
return {
|
|
actions,
|
|
remainingLog: remainingLines.join("\n").replace(/^\n+/, ""),
|
|
};
|
|
}
|
|
|
|
function SubAgentActionRow({ action }: { action: SubAgentActivityRow }) {
|
|
return (
|
|
<View style={styles.subAgentActionRow}>
|
|
<Text selectable style={styles.subAgentActionTool}>
|
|
{formatSubAgentToolName(action.toolName)}
|
|
</Text>
|
|
{action.summary ? (
|
|
<Text selectable style={styles.subAgentActionSummary}>
|
|
{action.summary}
|
|
</Text>
|
|
) : null}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function formatSubAgentToolName(toolName: string): string {
|
|
const trimmed = toolName.trim();
|
|
if (!trimmed) {
|
|
return toolName;
|
|
}
|
|
return trimmed
|
|
.replace(/[._-]+/g, " ")
|
|
.split(" ")
|
|
.filter((segment) => segment.length > 0)
|
|
.map((segment) => `${segment[0]?.toUpperCase() ?? ""}${segment.slice(1)}`)
|
|
.join(" ");
|
|
}
|
|
|
|
function SubAgentLogText({
|
|
activityLog,
|
|
fallbackHeader,
|
|
hasActions,
|
|
}: {
|
|
activityLog: string;
|
|
fallbackHeader: string;
|
|
hasActions: boolean;
|
|
}) {
|
|
if (activityLog.length > 0) {
|
|
return (
|
|
<Text selectable style={styles.scrollText}>
|
|
{activityLog}
|
|
</Text>
|
|
);
|
|
}
|
|
if (!hasActions) {
|
|
return (
|
|
<Text selectable style={styles.scrollText}>
|
|
{fallbackHeader}
|
|
</Text>
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function SubAgentDetailSection({
|
|
log,
|
|
childSessionId,
|
|
subAgentType,
|
|
description,
|
|
ds,
|
|
}: SubAgentDetailProps) {
|
|
const { actions, remainingLog } = useMemo(() => parseSubAgentLog(log), [log]);
|
|
const fallbackHeader = resolveSubAgentFallbackHeader(subAgentType, description);
|
|
const hasActions = actions.length > 0;
|
|
return (
|
|
<View style={ds.sectionFillStyle}>
|
|
<View style={ds.codeBlockFillStyle}>
|
|
<ScrollView
|
|
style={ds.codeVerticalScrollStyle}
|
|
contentContainerStyle={styles.codeVerticalContent}
|
|
nestedScrollEnabled
|
|
showsVerticalScrollIndicator
|
|
>
|
|
<ScrollView
|
|
horizontal
|
|
nestedScrollEnabled
|
|
showsHorizontalScrollIndicator
|
|
style={ds.webScrollbarStyle}
|
|
contentContainerStyle={styles.codeHorizontalContent}
|
|
>
|
|
<View style={styles.codeLine}>
|
|
{childSessionId ? (
|
|
<Text selectable style={styles.subAgentSessionText}>
|
|
session {childSessionId}
|
|
</Text>
|
|
) : null}
|
|
{hasActions ? (
|
|
<View style={styles.subAgentActions}>
|
|
{actions.map((action) => (
|
|
<SubAgentActionRow key={action.index} action={action} />
|
|
))}
|
|
</View>
|
|
) : null}
|
|
<SubAgentLogText
|
|
activityLog={remainingLog}
|
|
fallbackHeader={fallbackHeader}
|
|
hasActions={hasActions}
|
|
/>
|
|
</View>
|
|
</ScrollView>
|
|
</ScrollView>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
interface EditDetailProps {
|
|
diffLines: DiffLine[] | undefined;
|
|
ds: DetailStyles;
|
|
}
|
|
|
|
function EditDetailSection({ diffLines, ds }: EditDetailProps) {
|
|
return (
|
|
<View style={ds.sectionFillStyle}>
|
|
{diffLines ? (
|
|
<View style={ds.codeBlockFillStyle}>
|
|
<DiffViewer
|
|
diffLines={diffLines}
|
|
maxHeight={ds.resolvedMaxHeight}
|
|
fillAvailableHeight={ds.shouldFill}
|
|
/>
|
|
</View>
|
|
) : null}
|
|
</View>
|
|
);
|
|
}
|
|
|
|
interface ScrollableContentProps {
|
|
content: string;
|
|
ds: DetailStyles;
|
|
wrapInSectionFill?: boolean;
|
|
}
|
|
|
|
function ScrollableTextSection({ content, ds, wrapInSectionFill = true }: ScrollableContentProps) {
|
|
const body = (
|
|
<ScrollView
|
|
style={ds.scrollAreaFillStyle}
|
|
contentContainerStyle={styles.scrollContent}
|
|
nestedScrollEnabled
|
|
showsVerticalScrollIndicator={true}
|
|
>
|
|
<ScrollView
|
|
horizontal
|
|
nestedScrollEnabled
|
|
showsHorizontalScrollIndicator={true}
|
|
style={ds.webScrollbarStyle}
|
|
>
|
|
<Text selectable style={styles.scrollText}>
|
|
{content}
|
|
</Text>
|
|
</ScrollView>
|
|
</ScrollView>
|
|
);
|
|
if (!wrapInSectionFill) return body;
|
|
return <View style={ds.sectionFillStyle}>{body}</View>;
|
|
}
|
|
|
|
interface FetchDetailProps {
|
|
url: string;
|
|
result: string | null | undefined;
|
|
ds: DetailStyles;
|
|
}
|
|
|
|
function FetchDetailSection({ url, result, ds }: FetchDetailProps) {
|
|
return (
|
|
<View style={ds.sectionFillStyle}>
|
|
<ScrollView
|
|
style={ds.scrollAreaFillStyle}
|
|
contentContainerStyle={styles.scrollContent}
|
|
nestedScrollEnabled
|
|
showsVerticalScrollIndicator
|
|
>
|
|
<ScrollView
|
|
horizontal
|
|
nestedScrollEnabled
|
|
showsHorizontalScrollIndicator
|
|
style={ds.webScrollbarStyle}
|
|
>
|
|
<Text selectable style={styles.scrollText}>
|
|
{result ? `${url}\n\n${result}` : url}
|
|
</Text>
|
|
</ScrollView>
|
|
</ScrollView>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function PlainTextSection({ text }: { text: string }) {
|
|
return (
|
|
<View style={styles.plainTextSection}>
|
|
<Text selectable style={styles.plainText}>
|
|
{text}
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
interface SearchDetail {
|
|
query?: string;
|
|
content?: string;
|
|
filePaths?: string[];
|
|
webResults?: { title: string; url: string }[];
|
|
annotations?: string[];
|
|
}
|
|
|
|
function buildSearchSections(detail: SearchDetail, ds: DetailStyles): ReactNode[] {
|
|
const out: ReactNode[] = [];
|
|
if (detail.content) {
|
|
out.push(
|
|
<View key="search-content" style={styles.section}>
|
|
<ScrollView
|
|
style={ds.scrollAreaStyle}
|
|
contentContainerStyle={styles.scrollContent}
|
|
nestedScrollEnabled
|
|
showsVerticalScrollIndicator
|
|
>
|
|
<ScrollView
|
|
horizontal
|
|
nestedScrollEnabled
|
|
showsHorizontalScrollIndicator
|
|
style={ds.webScrollbarStyle}
|
|
>
|
|
<Text selectable style={styles.scrollText}>
|
|
{detail.content}
|
|
</Text>
|
|
</ScrollView>
|
|
</ScrollView>
|
|
</View>,
|
|
);
|
|
}
|
|
if (detail.filePaths && detail.filePaths.length > 0) {
|
|
out.push(
|
|
<View key="search-files" style={styles.section}>
|
|
<Text selectable style={styles.scrollText}>
|
|
{detail.filePaths.join("\n")}
|
|
</Text>
|
|
</View>,
|
|
);
|
|
}
|
|
if (detail.webResults && detail.webResults.length > 0) {
|
|
out.push(
|
|
<View key="search-web-results" style={styles.section}>
|
|
<Text selectable style={styles.scrollText}>
|
|
{detail.webResults.map((entry) => `${entry.title}\n${entry.url}`).join("\n\n")}
|
|
</Text>
|
|
</View>,
|
|
);
|
|
}
|
|
if (detail.annotations && detail.annotations.length > 0) {
|
|
out.push(
|
|
<View key="search-annotations" style={styles.section}>
|
|
<Text selectable style={styles.scrollText}>
|
|
{detail.annotations.join("\n\n")}
|
|
</Text>
|
|
</View>,
|
|
);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function serializeUnknownValue(value: unknown): string {
|
|
try {
|
|
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
} catch {
|
|
return String(value);
|
|
}
|
|
}
|
|
|
|
interface UnknownDetail {
|
|
input: unknown;
|
|
output: unknown;
|
|
}
|
|
|
|
function buildUnknownSections(detail: UnknownDetail, ds: DetailStyles): ReactNode[] {
|
|
const plainInputText =
|
|
typeof detail.input === "string" && detail.output === null ? detail.input : null;
|
|
|
|
if (plainInputText !== null) {
|
|
return [
|
|
<View key="unknown-plain-text" style={styles.plainTextSection}>
|
|
<Text selectable style={styles.plainText}>
|
|
{plainInputText}
|
|
</Text>
|
|
</View>,
|
|
];
|
|
}
|
|
|
|
const sectionsFromTopLevel = [
|
|
{ title: "Input", value: detail.input },
|
|
{ title: "Output", value: detail.output },
|
|
].filter((entry) =>
|
|
hasMeaningfulToolCallDetail({
|
|
type: "unknown",
|
|
input: entry.value ?? null,
|
|
output: null,
|
|
}),
|
|
);
|
|
|
|
const out: ReactNode[] = [];
|
|
for (const section of sectionsFromTopLevel) {
|
|
const value = serializeUnknownValue(section.value);
|
|
if (!value.length) {
|
|
continue;
|
|
}
|
|
out.push(
|
|
<View key={`${section.title}-header`} style={styles.groupHeader}>
|
|
<Text style={styles.groupHeaderText}>{section.title}</Text>
|
|
</View>,
|
|
);
|
|
out.push(
|
|
<View key={`${section.title}-value`} style={styles.section}>
|
|
<ScrollView
|
|
horizontal
|
|
nestedScrollEnabled
|
|
style={ds.jsonScrollCombined}
|
|
contentContainerStyle={styles.jsonContent}
|
|
showsHorizontalScrollIndicator={true}
|
|
>
|
|
<Text selectable style={styles.scrollText}>
|
|
{value}
|
|
</Text>
|
|
</ScrollView>
|
|
</View>,
|
|
);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
function buildDetailSections(
|
|
detail: ToolCallDetail | undefined,
|
|
diffLines: DiffLine[] | undefined,
|
|
ds: DetailStyles,
|
|
): ReactNode[] {
|
|
if (!detail) return [];
|
|
if (detail.type === "shell") {
|
|
return [
|
|
<ShellDetailSection key="shell" command={detail.command} output={detail.output} ds={ds} />,
|
|
];
|
|
}
|
|
if (detail.type === "worktree_setup") {
|
|
return [
|
|
<WorktreeSetupDetailSection
|
|
key="worktree-setup"
|
|
log={detail.log}
|
|
branchName={detail.branchName}
|
|
worktreePath={detail.worktreePath}
|
|
ds={ds}
|
|
/>,
|
|
];
|
|
}
|
|
if (detail.type === "sub_agent") {
|
|
return [
|
|
<SubAgentDetailSection
|
|
key="sub-agent"
|
|
log={detail.log}
|
|
childSessionId={detail.childSessionId}
|
|
subAgentType={detail.subAgentType}
|
|
description={detail.description}
|
|
ds={ds}
|
|
/>,
|
|
];
|
|
}
|
|
if (detail.type === "edit") {
|
|
return [<EditDetailSection key="edit" diffLines={diffLines} ds={ds} />];
|
|
}
|
|
if (detail.type === "write") {
|
|
return [
|
|
<View key="write" style={ds.sectionFillStyle}>
|
|
{detail.content ? (
|
|
<ScrollableTextSection content={detail.content} ds={ds} wrapInSectionFill={false} />
|
|
) : null}
|
|
</View>,
|
|
];
|
|
}
|
|
if (detail.type === "read") {
|
|
if (!detail.content) return [];
|
|
return [<ScrollableTextSection key="read" content={detail.content} ds={ds} />];
|
|
}
|
|
if (detail.type === "search") {
|
|
return buildSearchSections(detail, ds);
|
|
}
|
|
if (detail.type === "fetch") {
|
|
return [<FetchDetailSection key="fetch" url={detail.url} result={detail.result} ds={ds} />];
|
|
}
|
|
if (detail.type === "plain_text") {
|
|
if (!detail.text) return [];
|
|
return [<PlainTextSection key="plain-text" text={detail.text} />];
|
|
}
|
|
if (detail.type === "unknown") {
|
|
return buildUnknownSections(detail, ds);
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function ErrorSection({ errorText, ds }: { errorText: string; ds: DetailStyles }) {
|
|
return (
|
|
<View style={styles.section}>
|
|
<Text style={SECTION_TITLE_ERROR_STYLE}>Error</Text>
|
|
<ScrollView
|
|
horizontal
|
|
nestedScrollEnabled
|
|
style={ds.jsonScrollErrorCombined}
|
|
contentContainerStyle={styles.jsonContent}
|
|
showsHorizontalScrollIndicator={true}
|
|
>
|
|
<Text selectable style={SCROLL_TEXT_ERROR_STYLE}>
|
|
{errorText}
|
|
</Text>
|
|
</ScrollView>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
function LoadingSkeleton({ containerStyle }: { containerStyle: StyleProp<ViewStyle> }) {
|
|
return (
|
|
<View style={containerStyle}>
|
|
<View style={styles.loadingLineWide} />
|
|
<View style={styles.loadingLineMedium} />
|
|
<View style={styles.loadingLineShort} />
|
|
</View>
|
|
);
|
|
}
|
|
|
|
export function ToolCallDetailsContent({
|
|
detail,
|
|
errorText,
|
|
maxHeight,
|
|
fillAvailableHeight = false,
|
|
showLoadingSkeleton = false,
|
|
}: ToolCallDetailsContentProps) {
|
|
const resolvedMaxHeight = fillAvailableHeight ? undefined : (maxHeight ?? 300);
|
|
const ds = useDetailStyles(detail, resolvedMaxHeight, fillAvailableHeight);
|
|
const diffLines = useDiffLines(detail);
|
|
|
|
const sections: ReactNode[] = buildDetailSections(detail, diffLines, ds);
|
|
|
|
if (errorText) {
|
|
sections.push(<ErrorSection key="error" errorText={errorText} ds={ds} />);
|
|
}
|
|
|
|
if (sections.length === 0) {
|
|
if (showLoadingSkeleton) {
|
|
return <LoadingSkeleton containerStyle={ds.loadingContainerStyle} />;
|
|
}
|
|
return <Text style={styles.emptyStateText}>No additional details available</Text>;
|
|
}
|
|
|
|
return <View style={ds.fullBleedContainerStyle}>{sections}</View>;
|
|
}
|
|
|
|
// ---- Styles ----
|
|
|
|
const styles = StyleSheet.create((theme) => {
|
|
const insets = getCodeInsets(theme);
|
|
|
|
return {
|
|
paddedContainer: {
|
|
gap: theme.spacing[4],
|
|
padding: 0,
|
|
},
|
|
fullBleedContainer: {
|
|
gap: theme.spacing[2],
|
|
padding: 0,
|
|
},
|
|
groupHeader: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
paddingHorizontal: theme.spacing[3],
|
|
paddingVertical: theme.spacing[2],
|
|
borderBottomWidth: theme.borderWidth[1],
|
|
borderBottomColor: theme.colors.border,
|
|
},
|
|
groupHeaderText: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.sm,
|
|
fontWeight: theme.fontWeight.normal,
|
|
},
|
|
section: {
|
|
gap: theme.spacing[2],
|
|
},
|
|
fillHeight: {
|
|
flex: 1,
|
|
minHeight: 0,
|
|
},
|
|
plainTextSection: {
|
|
gap: theme.spacing[2],
|
|
padding: theme.spacing[3],
|
|
},
|
|
plainText: {
|
|
fontFamily: Fonts.sans,
|
|
fontSize: theme.fontSize.base,
|
|
color: theme.colors.foreground,
|
|
lineHeight: 22,
|
|
overflowWrap: "anywhere",
|
|
},
|
|
sectionTitle: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.xs,
|
|
fontWeight: theme.fontWeight.semibold,
|
|
textTransform: "uppercase",
|
|
letterSpacing: 0.5,
|
|
},
|
|
rangeText: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.xs,
|
|
},
|
|
diffContainer: {
|
|
borderWidth: theme.borderWidth[1],
|
|
borderColor: theme.colors.border,
|
|
borderRadius: theme.borderRadius.base,
|
|
overflow: "hidden",
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
fullBleedBlock: {
|
|
borderWidth: 0,
|
|
borderRadius: 0,
|
|
overflow: "hidden",
|
|
backgroundColor: theme.colors.surface1,
|
|
},
|
|
codeVerticalScroll: {},
|
|
codeVerticalContent: {
|
|
flexGrow: 1,
|
|
paddingBottom: insets.extraBottom,
|
|
},
|
|
codeHorizontalContent: {
|
|
paddingRight: insets.extraRight,
|
|
},
|
|
codeLine: {
|
|
minWidth: "100%",
|
|
paddingHorizontal: insets.padding,
|
|
paddingVertical: insets.padding,
|
|
},
|
|
scrollArea: {
|
|
borderWidth: theme.borderWidth[1],
|
|
borderColor: theme.colors.border,
|
|
borderRadius: theme.borderRadius.base,
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
scrollContent: {
|
|
padding: insets.padding,
|
|
},
|
|
scrollText: {
|
|
fontFamily: Fonts.mono,
|
|
fontSize: theme.fontSize.xs,
|
|
color: theme.colors.foreground,
|
|
lineHeight: 18,
|
|
...(isWeb
|
|
? {
|
|
whiteSpace: "pre",
|
|
overflowWrap: "normal",
|
|
}
|
|
: null),
|
|
},
|
|
shellPrompt: {
|
|
color: theme.colors.foregroundMuted,
|
|
},
|
|
subAgentSessionText: {
|
|
fontFamily: Fonts.mono,
|
|
fontSize: theme.fontSize.xs,
|
|
color: theme.colors.foregroundMuted,
|
|
lineHeight: 18,
|
|
marginBottom: theme.spacing[2],
|
|
},
|
|
subAgentActions: {
|
|
gap: theme.spacing[1],
|
|
marginBottom: theme.spacing[2],
|
|
},
|
|
subAgentActionRow: {
|
|
flexDirection: "row",
|
|
alignItems: "center",
|
|
gap: theme.spacing[2],
|
|
},
|
|
subAgentActionTool: {
|
|
fontFamily: Fonts.mono,
|
|
fontSize: theme.fontSize.xs,
|
|
color: theme.colors.foregroundMuted,
|
|
lineHeight: 18,
|
|
},
|
|
subAgentActionSummary: {
|
|
fontFamily: Fonts.mono,
|
|
fontSize: theme.fontSize.xs,
|
|
color: theme.colors.foreground,
|
|
lineHeight: 18,
|
|
},
|
|
jsonScroll: {
|
|
borderWidth: theme.borderWidth[1],
|
|
borderColor: theme.colors.border,
|
|
borderRadius: theme.borderRadius.base,
|
|
backgroundColor: theme.colors.surface2,
|
|
},
|
|
jsonScrollError: {
|
|
borderColor: theme.colors.destructive,
|
|
},
|
|
jsonContent: {
|
|
padding: insets.padding,
|
|
},
|
|
errorText: {
|
|
color: theme.colors.destructive,
|
|
},
|
|
emptyStateText: {
|
|
color: theme.colors.foregroundMuted,
|
|
fontSize: theme.fontSize.sm,
|
|
fontStyle: "italic",
|
|
},
|
|
loadingContainer: {
|
|
gap: theme.spacing[2],
|
|
padding: theme.spacing[3],
|
|
},
|
|
loadingLineWide: {
|
|
height: 12,
|
|
width: "100%",
|
|
borderRadius: theme.borderRadius.full,
|
|
backgroundColor: theme.colors.surface3,
|
|
},
|
|
loadingLineMedium: {
|
|
height: 12,
|
|
width: "72%",
|
|
borderRadius: theme.borderRadius.full,
|
|
backgroundColor: theme.colors.surface3,
|
|
},
|
|
loadingLineShort: {
|
|
height: 12,
|
|
width: "48%",
|
|
borderRadius: theme.borderRadius.full,
|
|
backgroundColor: theme.colors.surface3,
|
|
},
|
|
};
|
|
});
|
|
|
|
const SECTION_TITLE_ERROR_STYLE = [styles.sectionTitle, styles.errorText];
|
|
const SCROLL_TEXT_ERROR_STYLE = [styles.scrollText, styles.errorText];
|