diff --git a/features/app-settings/app-settings.screen.tsx b/features/app-settings/app-settings.screen.tsx index 14035c06..fa330c8a 100644 --- a/features/app-settings/app-settings.screen.tsx +++ b/features/app-settings/app-settings.screen.tsx @@ -23,8 +23,14 @@ export const AppSettingsScreen = memo(function AppSettingsScreen() { }) const generalSettings = useMemo((): ISettingsListRow[] => { - return [{ label: "Environment", value: getEnv() }].filter(Boolean) - }, []) + return [ + { label: "Environment", value: getEnv() }, + { + label: "Agent content examples", + onPress: () => router.navigate("AgentContentExamples"), + }, + ].filter(Boolean) + }, [router]) return ( +
+ +
+ + + {title} + {!!subtitle && ( + + {subtitle} + + )} + + + {trailing} + + {collapsible && ( + + )} + + ) + + return ( + + {collapsible ? ( + setExpanded((prev) => !prev)}> + {Header} + + ) : ( + Header + )} + + {(!collapsible || expanded) && !!children && ( + {children} + )} + + ) +}) + +function getAccentColor(args: { + accent: IAgentCardAccent + theme: ReturnType["theme"] +}) { + const { accent, theme } = args + switch (accent) { + case "accent": + return theme.colors.fill.accent + case "green": + return theme.colors.global.green + case "caution": + return theme.colors.global.caution + case "neutral": + default: + return theme.colors.text.secondary + } +} + +// Lightweight translucent background for the icon badge. Colors in the palette +// are hex strings, so append an alpha channel. +function withAlpha(color: string) { + if (color.startsWith("#") && color.length === 7) { + return `${color}1A` // ~10% opacity + } + return color +} + +const $card: ThemedStyle = ({ colors, spacing, borderRadius, borderWidth }) => ({ + width: "100%", + borderRadius: borderRadius.sm, + borderWidth: borderWidth.sm, + borderColor: colors.border.subtle, + backgroundColor: colors.background.surface, + paddingVertical: spacing.xs, + paddingHorizontal: spacing.sm, + rowGap: spacing.xs, +}) + +const $header: ThemedStyle = ({ spacing }) => ({ + alignItems: "center", + columnGap: spacing.xs, +}) + +const $iconBadge: ThemedStyle = ({ spacing, borderRadius }) => ({ + width: spacing.lg, + height: spacing.lg, + borderRadius: borderRadius.xs, +}) + +const $headerText: ThemedStyle = ({ spacing }) => ({ + flex: 1, + rowGap: spacing["6xs"], +}) + +const $body: ThemedStyle = ({ spacing }) => ({ + rowGap: spacing.xs, +}) diff --git a/features/conversation/conversation-chat/conversation-message/agent-content/agent-content-action.tsx b/features/conversation/conversation-chat/conversation-message/agent-content/agent-content-action.tsx new file mode 100644 index 00000000..025d863c --- /dev/null +++ b/features/conversation/conversation-chat/conversation-message/agent-content/agent-content-action.tsx @@ -0,0 +1,172 @@ +import { memo } from "react" +import { ViewStyle } from "react-native" +import { ActivityIndicator } from "@/design-system/activity-indicator" +import { Button } from "@/design-system/Button/Button" +import { HStack } from "@/design-system/HStack" +import { Icon } from "@/design-system/Icon/Icon" +import { Text } from "@/design-system/Text" +import { VStack } from "@/design-system/VStack" +import { + AgentCard, + IAgentCardAccent, +} from "@/features/conversation/conversation-chat/conversation-message/agent-content/agent-card" +import { + IAgentActionContent, + IAgentActionStatus, +} from "@/features/conversation/conversation-chat/conversation-message/agent-content/agent-content.types" +import { ThemedStyle, useAppTheme } from "@/theme/use-app-theme" + +type IAgentContentActionProps = { + content: IAgentActionContent + onApprove?: () => void + onDecline?: () => void +} + +/** + * Bucket 3 — Action / Tool call. + * + * Surfaces an action the agent took or wants to take. When the action requires + * approval we show Approve / Decline so the user stays in control. + */ +export const AgentContentAction = memo(function AgentContentAction( + props: IAgentContentActionProps, +) { + const { content, onApprove, onDecline } = props + const { theme, themed } = useAppTheme() + + const accent = getAccentForStatus(content.status) + const needsApproval = content.requiresApproval && content.status === "proposed" + + return ( + } + > + {!!content.description && ( + + {content.description} + + )} + + {!!content.params?.length && ( + + {content.params.map((param) => ( + + + {param.label} + + + {param.value} + + + ))} + + )} + + {!!content.resultSummary && ( + + + + {content.resultSummary} + + + )} + + {needsApproval && ( + +