Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions features/app-settings/app-settings.screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<Screen
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { memo, ReactNode, useState } from "react"
import { ViewStyle } from "react-native"
import { Center } from "@/design-system/Center"
import { HStack } from "@/design-system/HStack"
import { Icon } from "@/design-system/Icon/Icon"
import { IIconName } from "@/design-system/Icon/Icon.types"
import { Pressable } from "@/design-system/Pressable"
import { Text } from "@/design-system/Text"
import { VStack } from "@/design-system/VStack"
import { ThemedStyle, useAppTheme } from "@/theme/use-app-theme"

export type IAgentCardAccent = "neutral" | "accent" | "green" | "caution"

type IAgentCardProps = {
icon: IIconName
title: string
subtitle?: string
accent?: IAgentCardAccent
/** Optional content rendered on the right side of the header (e.g. a status chip). */
trailing?: ReactNode
/** When set, the card header becomes pressable and toggles the body. */
collapsible?: boolean
defaultExpanded?: boolean
children?: ReactNode
}

/**
* Shared container for all agent "superpower" content cards.
*
* Renders a full-width, lightly-bordered card with a colored icon badge, a
* title/subtitle header and an optional collapsible body. Kept local to the
* agent-content module so the visual language of these cards can evolve
* independently from chat bubbles.
*/
export const AgentCard = memo(function AgentCard(props: IAgentCardProps) {
const {
icon,
title,
subtitle,
accent = "neutral",
trailing,
collapsible = false,
defaultExpanded = true,
children,
} = props

const { theme, themed } = useAppTheme()
const [expanded, setExpanded] = useState(defaultExpanded)

const accentColor = getAccentColor({ accent, theme })

const Header = (
<HStack style={themed($header)}>
<Center style={[themed($iconBadge), { backgroundColor: withAlpha(accentColor) }]}>
<Icon icon={icon} size={theme.iconSize.sm} color={accentColor} />
</Center>

<VStack style={themed($headerText)}>
<Text preset="smallerBold">{title}</Text>
{!!subtitle && (
<Text preset="smaller" color="secondary">
{subtitle}
</Text>
)}
</VStack>

{trailing}

{collapsible && (
<Icon
icon={expanded ? "chevron.up" : "chevron.down"}
size={theme.iconSize.sm}
color={theme.colors.text.tertiary}
/>
)}
</HStack>
)

return (
<VStack style={themed($card)}>
{collapsible ? (
<Pressable withHaptics onPress={() => setExpanded((prev) => !prev)}>
{Header}
</Pressable>
) : (
Header
)}

{(!collapsible || expanded) && !!children && (
<VStack style={themed($body)}>{children}</VStack>
)}
</VStack>
)
})

function getAccentColor(args: {
accent: IAgentCardAccent
theme: ReturnType<typeof useAppTheme>["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<ViewStyle> = ({ 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<ViewStyle> = ({ spacing }) => ({
alignItems: "center",
columnGap: spacing.xs,
})

const $iconBadge: ThemedStyle<ViewStyle> = ({ spacing, borderRadius }) => ({
width: spacing.lg,
height: spacing.lg,
borderRadius: borderRadius.xs,
})

const $headerText: ThemedStyle<ViewStyle> = ({ spacing }) => ({
flex: 1,
rowGap: spacing["6xs"],
})

const $body: ThemedStyle<ViewStyle> = ({ spacing }) => ({
rowGap: spacing.xs,
})
Original file line number Diff line number Diff line change
@@ -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 (
<AgentCard
icon="arrow.up.right"
accent={accent}
title={content.title}
subtitle={content.toolName}
trailing={<ActionStatusBadge status={content.status} />}
>
{!!content.description && (
<Text preset="small" color="secondary">
{content.description}
</Text>
)}

{!!content.params?.length && (
<VStack style={themed($params)}>
{content.params.map((param) => (
<HStack key={param.label} style={themed($paramRow)}>
<Text preset="smaller" color="secondary">
{param.label}
</Text>
<Text preset="smaller" weight="medium" numberOfLines={1} style={themed($paramValue)}>
{param.value}
</Text>
</HStack>
))}
</VStack>
)}

{!!content.resultSummary && (
<HStack style={themed($result)}>
<Icon
icon={content.status === "error" ? "exclamationmark.triangle" : "checkmark"}
size={theme.iconSize.xs}
color={
content.status === "error" ? theme.colors.global.caution : theme.colors.global.green
}
/>
<Text preset="smaller" color="secondary" style={{ flex: 1 }}>
{content.resultSummary}
</Text>
</HStack>
)}

{needsApproval && (
<HStack style={themed($actions)}>
<Button variant="fill" size="sm" text="Approve" onPress={onApprove} style={{ flex: 1 }} />
<Button
variant="outline"
size="sm"
text="Decline"
onPress={onDecline}
style={{ flex: 1 }}
/>
</HStack>
)}
</AgentCard>
)
})

const ActionStatusBadge = memo(function ActionStatusBadge(props: { status: IAgentActionStatus }) {
const { status } = props
const { theme } = useAppTheme()

if (status === "running") {
return <ActivityIndicator size="small" color={theme.colors.fill.accent} />
}

const { label, color } = getStatusLabel({ status, theme })

return (
<Text preset="smaller" weight="medium" style={{ color }}>
{label}
</Text>
)
})

function getAccentForStatus(status: IAgentActionStatus): IAgentCardAccent {
switch (status) {
case "success":
return "green"
case "error":
return "caution"
case "running":
case "proposed":
default:
return "accent"
}
}

function getStatusLabel(args: {
status: IAgentActionStatus
theme: ReturnType<typeof useAppTheme>["theme"]
}) {
const { status, theme } = args
switch (status) {
case "success":
return { label: "Done", color: theme.colors.global.green }
case "error":
return { label: "Failed", color: theme.colors.global.caution }
case "proposed":
return { label: "Proposed", color: theme.colors.text.secondary }
case "running":
default:
return { label: "Running", color: theme.colors.text.secondary }
}
}

const $params: ThemedStyle<ViewStyle> = ({ spacing, colors, borderRadius }) => ({
rowGap: spacing.xxs,
padding: spacing.xs,
borderRadius: borderRadius.xs,
backgroundColor: colors.fill.minimal,
})

const $paramRow: ThemedStyle<ViewStyle> = ({ spacing }) => ({
justifyContent: "space-between",
columnGap: spacing.sm,
})

const $paramValue: ThemedStyle<ViewStyle> = () => ({
flexShrink: 1,
textAlign: "right",
})

const $result: ThemedStyle<ViewStyle> = ({ spacing }) => ({
columnGap: spacing.xxs,
alignItems: "center",
})

const $actions: ThemedStyle<ViewStyle> = ({ spacing }) => ({
columnGap: spacing.xs,
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { memo, useCallback, useMemo } from "react"
import { Screen } from "@/components/screen/screen"
import { IExtendedEdge } from "@/components/screen/screen.helpers"
import { IHeaderProps } from "@/design-system/Header/Header"
import { IIconName } from "@/design-system/Icon/Icon.types"
import { Text } from "@/design-system/Text"
import { VStack } from "@/design-system/VStack"
import { AgentContent } from "@/features/conversation/conversation-chat/conversation-message/agent-content/agent-content"
import { allAgentContentFixtures } from "@/features/conversation/conversation-chat/conversation-message/agent-content/agent-content.fixtures"
import { useHeader } from "@/navigation/use-header"
import { useRouter } from "@/navigation/use-navigation"
import { useAppTheme } from "@/theme/use-app-theme"

/**
* Dev/demo screen that renders every agent content card with mock data so the
* five "superpower" buckets can be reviewed without protocol plumbing.
*/
Comment on lines +14 to +17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove verbose JSDoc on the screen component.

This comment is descriptive but redundant with the component name and implementation; it adds maintenance overhead when behavior evolves.

✂️ Suggested change
-/**
- * Dev/demo screen that renders every agent content card with mock data so the
- * five "superpower" buckets can be reviewed without protocol plumbing.
- */
 export const AgentContentExamplesScreen = memo(function AgentContentExamplesScreen() {

As per coding guidelines, "**/*.{ts,tsx}: Don't add comments for obvious code ... Avoid verbose JSDoc-style documentation when TypeScript types already provide this information."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Dev/demo screen that renders every agent content card with mock data so the
* five "superpower" buckets can be reviewed without protocol plumbing.
*/
export const AgentContentExamplesScreen = memo(function AgentContentExamplesScreen() {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@features/conversation/conversation-chat/conversation-message/agent-content/agent-content-examples.screen.tsx`
around lines 14 - 17, Remove the verbose JSDoc comment block (the multi-line
comment starting with /** and ending with */) at the top of the
agent-content-examples.screen.tsx file. The component name already clearly
conveys its purpose as a demo screen for agent content examples, making the
detailed explanation redundant and unnecessary to maintain according to the
project's coding guidelines.

Source: Coding guidelines

export const AgentContentExamplesScreen = memo(function AgentContentExamplesScreen() {
const { theme } = useAppTheme()
const router = useRouter()

const handleBackPress = useCallback(() => {
router.goBack()
}, [router])

const headerOptions = useMemo(() => {
return {
safeAreaEdges: ["top"] as IExtendedEdge[],
title: "Agent content",
leftIcon: "chevron.left" as IIconName,
onLeftPress: handleBackPress,
} satisfies IHeaderProps
}, [handleBackPress])

useHeader(headerOptions, [headerOptions])

return (
<Screen preset="scroll" contentContainerStyle={{ padding: theme.spacing.md }}>
<VStack style={{ rowGap: theme.spacing.md }}>
<Text preset="formLabel" color="secondary">
The five agent "superpower" content types, rendered with mock data.
</Text>

{allAgentContentFixtures.map((agentContent, index) => (
<AgentContent key={index} agentContent={agentContent} />
))}
</VStack>
</Screen>
)
})
Loading
Loading