@vvedantb/native-ui 0.12.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -19
- package/package.json +36 -3
- package/src/BottomSheetContainer.tsx +2 -2
- package/src/BulkActionBar.tsx +240 -105
- package/src/Button.tsx +11 -7
- package/src/ConfirmDialog.tsx +25 -35
- package/src/Dialog.tsx +47 -35
- package/src/DividedList.tsx +11 -7
- package/src/DropdownMenu.tsx +22 -8
- package/src/FloatingSurface.tsx +5 -2
- package/src/FloatingTabBar.tsx +13 -22
- package/src/HeaderIconButton.tsx +4 -3
- package/src/Input.tsx +2 -2
- package/src/LoadingState.tsx +132 -0
- package/src/PageTitle.tsx +9 -2
- package/src/Pager.tsx +62 -0
- package/src/PasswordInput.tsx +1 -1
- package/src/PillTabs.tsx +144 -34
- package/src/Progress.tsx +9 -7
- package/src/Select.tsx +4 -4
- package/src/Sheet.tsx +29 -22
- package/src/SheetRow.tsx +27 -9
- package/src/Skeleton.tsx +91 -9
- package/src/SurfaceRow.tsx +111 -0
- package/src/SwipeableRow.tsx +43 -0
- package/src/Switch.tsx +6 -4
- package/src/Tabs.tsx +68 -13
- package/src/Toast.tsx +107 -65
- package/src/ai/activity-step-detail.tsx +1 -3
- package/src/ai/activity-step-label.ts +981 -0
- package/src/ai/activity-step-types.ts +107 -0
- package/src/ai/activity-tasks-utils.ts +199 -0
- package/src/ai/activity-timeline.ts +45 -0
- package/src/ai/attachments.tsx +3 -1
- package/src/ai/card-button.tsx +49 -0
- package/src/ai/chat-text.ts +13 -7
- package/src/ai/chat-thread.tsx +34 -27
- package/src/ai/choice-question.tsx +154 -35
- package/src/ai/code-block.tsx +189 -49
- package/src/ai/collapsible.tsx +19 -13
- package/src/ai/conversation.tsx +390 -57
- package/src/ai/icon-button.tsx +41 -6
- package/src/ai/index.ts +52 -1
- package/src/ai/message-action-sheet.tsx +168 -0
- package/src/ai/message-quote.tsx +98 -0
- package/src/ai/message.tsx +226 -31
- package/src/ai/prompt-input-menu.tsx +197 -0
- package/src/ai/prompt-input.tsx +308 -73
- package/src/ai/reply-quote-chip.tsx +90 -0
- package/src/ai/shimmer.tsx +4 -6
- package/src/ai/subagent-cards.tsx +209 -0
- package/src/ai/tool-approval-card.tsx +162 -0
- package/src/ai/trace.tsx +54 -17
- package/src/ai/unified-diff.tsx +182 -0
- package/src/auth/GoogleSignInButton.tsx +6 -3
- package/src/auth/SignInScreen.tsx +11 -2
- package/src/brand/ProviderIcon.tsx +136 -0
- package/src/brand/index.ts +2 -0
- package/src/configureTheme.ts +39 -11
- package/src/controlStyles.ts +46 -30
- package/src/entries/gesture-handler.ts +4 -0
- package/src/entries/keyboard-controller.ts +4 -0
- package/src/entries/markdown.ts +1 -1
- package/src/entries/primitives.ts +24 -3
- package/src/entries/theme.ts +2 -1
- package/src/entries/video.ts +4 -0
- package/src/entries/webview.ts +6 -0
- package/src/floating.ts +25 -4
- package/src/haptics.ts +30 -4
- package/src/highlight.ts +302 -1
- package/src/index.ts +27 -5
- package/src/loadingGrid.ts +75 -0
- package/src/markdown/Markdown.tsx +234 -32
- package/src/markdown/MarkdownTable.tsx +188 -0
- package/src/markdown/tableText.ts +58 -0
- package/src/marketing/UpgradeToProCard.tsx +4 -3
- package/src/media/ImageGallery.tsx +224 -0
- package/src/media/MediaCard.tsx +100 -0
- package/src/media/imageGrid.ts +51 -0
- package/src/motion.ts +33 -0
- package/src/pagerSwipe.ts +102 -0
- package/src/palette.ts +8 -1
- package/src/primitives/button.tsx +4 -1
- package/src/primitives/keyboard-controller.tsx +106 -0
- package/src/primitives/keyboard-parts.tsx +104 -0
- package/src/primitives/overlay.tsx +133 -45
- package/src/primitives/pager-gesture.tsx +131 -0
- package/src/primitives/sheet-drag.tsx +305 -0
- package/src/primitives/sheet-gesture.tsx +181 -0
- package/src/primitives/swipe-row-gesture.tsx +290 -0
- package/src/primitives/toast-drag-gesture.tsx +92 -0
- package/src/primitives/web-click-guard.ts +50 -0
- package/src/relativeTime.ts +94 -0
- package/src/router/RouterStackPageHeader.tsx +2 -2
- package/src/settings/SettingsEmptyState.tsx +20 -10
- package/src/settings/SettingsField.tsx +11 -5
- package/src/settings/SettingsSection.tsx +8 -5
- package/src/settings/SettingsToggleRow.tsx +55 -22
- package/src/settings/relaxedSizeStyle.ts +11 -0
- package/src/sheetDrag.ts +67 -0
- package/src/swipeRow.ts +169 -0
- package/src/tabBarRoutes.ts +18 -0
- package/src/tabIndicator.ts +71 -0
- package/src/tailwind-preset.cjs +13 -6
- package/src/tailwind-preset.d.cts +5 -0
- package/src/theme.tsx +6 -2
- package/src/toastDrag.ts +58 -0
- package/src/typography.ts +20 -3
- package/src/useScheme.ts +10 -1
- package/src/video/InlineVideo.tsx +91 -0
- package/src/webview/HtmlView.tsx +91 -0
- package/src/webview/HtmlView.web.tsx +21 -0
- package/src/webview/htmlView.ts +45 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Text, View } from "react-native";
|
|
2
|
+
import { IconRobot } from "@tabler/icons-react-native";
|
|
3
|
+
import { cn } from "../cn";
|
|
4
|
+
import { cardStyle, sizeStyle, textStyle, typeStyle } from "../controlStyles";
|
|
5
|
+
import { palette, withAlpha } from "../palette";
|
|
6
|
+
import { useLargerText } from "../theme";
|
|
7
|
+
import { tokens } from "../tokens";
|
|
8
|
+
import { useScheme } from "../useScheme";
|
|
9
|
+
import { CardButton } from "./card-button";
|
|
10
|
+
import { chatTextSize, formatElapsed } from "./chat-text";
|
|
11
|
+
import {
|
|
12
|
+
Collapsible,
|
|
13
|
+
CollapsibleChevron,
|
|
14
|
+
CollapsibleContent,
|
|
15
|
+
CollapsibleTrigger,
|
|
16
|
+
useCollapsible,
|
|
17
|
+
} from "./collapsible";
|
|
18
|
+
import { ResponseText } from "./response-text";
|
|
19
|
+
|
|
20
|
+
export type SubagentStatus = "running" | "done" | "failed" | "cancelled";
|
|
21
|
+
|
|
22
|
+
const STATUS_LABEL: Record<SubagentStatus, string> = {
|
|
23
|
+
running: "Running",
|
|
24
|
+
done: "Done",
|
|
25
|
+
failed: "Failed",
|
|
26
|
+
cancelled: "Cancelled",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/** Pill colours per status. Text and border share one hue, the fill is a wash of it. */
|
|
30
|
+
export function subagentStatusColors(status: SubagentStatus, scheme: "light" | "dark") {
|
|
31
|
+
const hex =
|
|
32
|
+
status === "running"
|
|
33
|
+
? palette.primary[scheme]
|
|
34
|
+
: status === "done"
|
|
35
|
+
? palette.success[scheme]
|
|
36
|
+
: status === "failed"
|
|
37
|
+
? palette.danger[scheme]
|
|
38
|
+
: tokens.color.muted[scheme];
|
|
39
|
+
return { color: hex, borderColor: withAlpha(hex, 0.25), backgroundColor: withAlpha(hex, 0.12) };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function StatusPill({ status, testID }: { status: SubagentStatus; testID: string }) {
|
|
43
|
+
const scheme = useScheme();
|
|
44
|
+
const largerText = useLargerText();
|
|
45
|
+
const caption = chatTextSize("caption", largerText);
|
|
46
|
+
const colors = subagentStatusColors(status, scheme);
|
|
47
|
+
return (
|
|
48
|
+
<View
|
|
49
|
+
testID={testID}
|
|
50
|
+
accessible
|
|
51
|
+
accessibilityLabel={`Status: ${STATUS_LABEL[status]}`}
|
|
52
|
+
className="rounded-full border px-2.5 py-0.5"
|
|
53
|
+
style={{ borderColor: colors.borderColor, backgroundColor: colors.backgroundColor }}
|
|
54
|
+
>
|
|
55
|
+
<Text className={cn(caption, "font-instrument-sans-semibold")} style={[sizeStyle(caption), typeStyle("semibold"), { color: colors.color }]}>
|
|
56
|
+
{STATUS_LABEL[status]}
|
|
57
|
+
</Text>
|
|
58
|
+
</View>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type SubagentRunCardProps = {
|
|
63
|
+
title: string;
|
|
64
|
+
status: SubagentStatus;
|
|
65
|
+
/** Whole seconds the run has taken. Shown as "12s" or "1m 5s". */
|
|
66
|
+
elapsedSeconds?: number;
|
|
67
|
+
/** Shows a Cancel button while `status` is `running`. */
|
|
68
|
+
onCancel?: () => void;
|
|
69
|
+
className?: string;
|
|
70
|
+
/** Default `subagent-run-card`. Parts add `-status`, `-elapsed` and `-cancel`. */
|
|
71
|
+
testID?: string;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** A subagent run: title, status pill, elapsed time, and Cancel while it runs. */
|
|
75
|
+
export function SubagentRunCard({
|
|
76
|
+
title,
|
|
77
|
+
status,
|
|
78
|
+
elapsedSeconds,
|
|
79
|
+
onCancel,
|
|
80
|
+
className,
|
|
81
|
+
testID = "subagent-run-card",
|
|
82
|
+
}: SubagentRunCardProps) {
|
|
83
|
+
const scheme = useScheme();
|
|
84
|
+
const largerText = useLargerText();
|
|
85
|
+
const body = chatTextSize("body", largerText);
|
|
86
|
+
const small = chatTextSize("small", largerText);
|
|
87
|
+
const elapsed = elapsedSeconds == null ? null : formatElapsed(elapsedSeconds);
|
|
88
|
+
return (
|
|
89
|
+
<View
|
|
90
|
+
testID={testID}
|
|
91
|
+
accessible={false}
|
|
92
|
+
accessibilityLabel={`Subagent ${title}, ${STATUS_LABEL[status]}`}
|
|
93
|
+
className={cn("w-full gap-3 p-4", className)}
|
|
94
|
+
style={cardStyle(scheme)}
|
|
95
|
+
>
|
|
96
|
+
<View className="flex-row items-center gap-2">
|
|
97
|
+
<IconRobot size={largerText ? 22 : 18} color={tokens.color.muted[scheme]} />
|
|
98
|
+
<Text
|
|
99
|
+
numberOfLines={2}
|
|
100
|
+
className={cn(body, "min-w-0 flex-1 font-instrument-sans-semibold")}
|
|
101
|
+
style={[sizeStyle(body), typeStyle("semibold"), textStyle("foreground", scheme)]}
|
|
102
|
+
>
|
|
103
|
+
{title}
|
|
104
|
+
</Text>
|
|
105
|
+
<StatusPill status={status} testID={`${testID}-status`} />
|
|
106
|
+
</View>
|
|
107
|
+
<View className="flex-row flex-wrap items-center justify-between gap-2">
|
|
108
|
+
{elapsed ? (
|
|
109
|
+
<Text
|
|
110
|
+
testID={`${testID}-elapsed`}
|
|
111
|
+
accessibilityLabel={`Elapsed ${elapsed}`}
|
|
112
|
+
className={small}
|
|
113
|
+
style={[sizeStyle(small), textStyle("muted", scheme), { fontVariant: ["tabular-nums"] }]}
|
|
114
|
+
>
|
|
115
|
+
{elapsed}
|
|
116
|
+
</Text>
|
|
117
|
+
) : (
|
|
118
|
+
<View />
|
|
119
|
+
)}
|
|
120
|
+
{status === "running" && onCancel ? (
|
|
121
|
+
<CardButton
|
|
122
|
+
testID={`${testID}-cancel`}
|
|
123
|
+
label="Cancel"
|
|
124
|
+
accessibilityLabel={`Cancel ${title}`}
|
|
125
|
+
variant="outline"
|
|
126
|
+
onPress={onCancel}
|
|
127
|
+
/>
|
|
128
|
+
) : null}
|
|
129
|
+
</View>
|
|
130
|
+
</View>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type SubagentResultCardProps = {
|
|
135
|
+
title: string;
|
|
136
|
+
/** The short result, always shown. */
|
|
137
|
+
summary: string;
|
|
138
|
+
/** The full result, shown when the card is open. No fold without it. */
|
|
139
|
+
detail?: string;
|
|
140
|
+
defaultOpen?: boolean;
|
|
141
|
+
open?: boolean;
|
|
142
|
+
onOpenChange?: (open: boolean) => void;
|
|
143
|
+
className?: string;
|
|
144
|
+
/** Default `subagent-result-card`. Parts add `-trigger` and `-detail`. */
|
|
145
|
+
testID?: string;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/** A subagent's result: the summary, and the detail behind a "Show details" fold. */
|
|
149
|
+
export function SubagentResultCard({
|
|
150
|
+
title,
|
|
151
|
+
summary,
|
|
152
|
+
detail,
|
|
153
|
+
defaultOpen = false,
|
|
154
|
+
open,
|
|
155
|
+
onOpenChange,
|
|
156
|
+
className,
|
|
157
|
+
testID = "subagent-result-card",
|
|
158
|
+
}: SubagentResultCardProps) {
|
|
159
|
+
const scheme = useScheme();
|
|
160
|
+
const largerText = useLargerText();
|
|
161
|
+
const body = chatTextSize("body", largerText);
|
|
162
|
+
const small = chatTextSize("small", largerText);
|
|
163
|
+
return (
|
|
164
|
+
<Collapsible
|
|
165
|
+
testID={testID}
|
|
166
|
+
defaultOpen={defaultOpen}
|
|
167
|
+
open={open}
|
|
168
|
+
onOpenChange={onOpenChange}
|
|
169
|
+
className={cn("w-full gap-2 p-4", className)}
|
|
170
|
+
style={cardStyle(scheme)}
|
|
171
|
+
>
|
|
172
|
+
<Text
|
|
173
|
+
numberOfLines={2}
|
|
174
|
+
className={cn(body, "font-instrument-sans-semibold")}
|
|
175
|
+
style={[sizeStyle(body), typeStyle("semibold"), textStyle("foreground", scheme)]}
|
|
176
|
+
>
|
|
177
|
+
{title}
|
|
178
|
+
</Text>
|
|
179
|
+
<ResponseText>{summary}</ResponseText>
|
|
180
|
+
{detail ? (
|
|
181
|
+
<>
|
|
182
|
+
<CollapsibleTrigger
|
|
183
|
+
testID={`${testID}-trigger`}
|
|
184
|
+
accessibilityLabel={`Details for ${title}`}
|
|
185
|
+
className="min-h-11 flex-row items-center gap-1.5 self-start"
|
|
186
|
+
>
|
|
187
|
+
<ToggleLabel small={small} />
|
|
188
|
+
</CollapsibleTrigger>
|
|
189
|
+
<CollapsibleContent testID={`${testID}-detail`}>
|
|
190
|
+
<ResponseText>{detail}</ResponseText>
|
|
191
|
+
</CollapsibleContent>
|
|
192
|
+
</>
|
|
193
|
+
) : null}
|
|
194
|
+
</Collapsible>
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function ToggleLabel({ small }: { small: ReturnType<typeof chatTextSize> }) {
|
|
199
|
+
const { open } = useCollapsible();
|
|
200
|
+
const scheme = useScheme();
|
|
201
|
+
return (
|
|
202
|
+
<>
|
|
203
|
+
<Text className={cn(small, "font-instrument-sans")} style={[sizeStyle(small), textStyle("muted", scheme)]}>
|
|
204
|
+
{open ? "Hide details" : "Show details"}
|
|
205
|
+
</Text>
|
|
206
|
+
<CollapsibleChevron open={open} size={14} color={tokens.color.muted[scheme]} />
|
|
207
|
+
</>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { Text, View } from "react-native";
|
|
2
|
+
import { IconTool } from "@tabler/icons-react-native";
|
|
3
|
+
import { cn } from "../cn";
|
|
4
|
+
import { cardStyle, sizeStyle, textStyle, typeStyle } from "../controlStyles";
|
|
5
|
+
import { withAlpha, palette } from "../palette";
|
|
6
|
+
import { useLargerText } from "../theme";
|
|
7
|
+
import { tokens } from "../tokens";
|
|
8
|
+
import { useScheme } from "../useScheme";
|
|
9
|
+
import { CardButton } from "./card-button";
|
|
10
|
+
import { chatTextSize, MONO_FONT } from "./chat-text";
|
|
11
|
+
|
|
12
|
+
/** What the reader chose. `edit` is offered by `ToolDraftCard` only. */
|
|
13
|
+
export type ToolDecision = "approve" | "deny" | "edit";
|
|
14
|
+
|
|
15
|
+
export type ToolApprovalCardProps = {
|
|
16
|
+
/** The tool the model wants to run. */
|
|
17
|
+
toolName: string;
|
|
18
|
+
/** The arguments, as a short string or an object. Shown cut to `ARGS_MAX_CHARS`. */
|
|
19
|
+
args?: string | Record<string, unknown>;
|
|
20
|
+
/** Called with the choice. */
|
|
21
|
+
onDecide?: (decision: ToolDecision) => void;
|
|
22
|
+
/** True while the app sends the decision: the buttons are off and the card says so. */
|
|
23
|
+
pending?: boolean;
|
|
24
|
+
/** Turns the buttons off without the pending state. */
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
approveLabel?: string;
|
|
27
|
+
denyLabel?: string;
|
|
28
|
+
className?: string;
|
|
29
|
+
/** Default `tool-approval-card`. The buttons add `-approve`, `-deny` and `-edit`. */
|
|
30
|
+
testID?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Characters of the args summary before it is cut with an ellipsis. */
|
|
34
|
+
export const ARGS_MAX_CHARS = 160;
|
|
35
|
+
|
|
36
|
+
/** One line summary of tool arguments, cut at `max` characters. */
|
|
37
|
+
export function summarizeArgs(args: string | Record<string, unknown> | undefined, max: number = ARGS_MAX_CHARS): string {
|
|
38
|
+
if (args == null) return "";
|
|
39
|
+
let text: string;
|
|
40
|
+
if (typeof args === "string") {
|
|
41
|
+
text = args;
|
|
42
|
+
} else {
|
|
43
|
+
try {
|
|
44
|
+
text = JSON.stringify(args) ?? "";
|
|
45
|
+
} catch {
|
|
46
|
+
text = "[unserialisable arguments]";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
text = text.replace(/\s+/g, " ").trim();
|
|
50
|
+
return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}…` : text;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type BaseProps = ToolApprovalCardProps & { allowEdit?: boolean; editLabel?: string; heading: string };
|
|
54
|
+
|
|
55
|
+
function ToolCardBase({
|
|
56
|
+
toolName,
|
|
57
|
+
args,
|
|
58
|
+
onDecide,
|
|
59
|
+
pending = false,
|
|
60
|
+
disabled = false,
|
|
61
|
+
approveLabel = "Approve",
|
|
62
|
+
denyLabel = "Deny",
|
|
63
|
+
editLabel = "Edit",
|
|
64
|
+
allowEdit = false,
|
|
65
|
+
heading,
|
|
66
|
+
className,
|
|
67
|
+
testID = "tool-approval-card",
|
|
68
|
+
}: BaseProps) {
|
|
69
|
+
const scheme = useScheme();
|
|
70
|
+
const largerText = useLargerText();
|
|
71
|
+
const body = chatTextSize("body", largerText);
|
|
72
|
+
const small = chatTextSize("small", largerText);
|
|
73
|
+
const caption = chatTextSize("caption", largerText);
|
|
74
|
+
const off = pending || disabled;
|
|
75
|
+
const summary = summarizeArgs(args);
|
|
76
|
+
return (
|
|
77
|
+
<View
|
|
78
|
+
testID={testID}
|
|
79
|
+
accessible={false}
|
|
80
|
+
accessibilityLabel={`${heading}: ${toolName}`}
|
|
81
|
+
className={cn("w-full gap-3 p-4", className)}
|
|
82
|
+
style={[
|
|
83
|
+
cardStyle(scheme),
|
|
84
|
+
{ borderWidth: 1, borderColor: withAlpha(palette.primary[scheme], 0.25) },
|
|
85
|
+
]}
|
|
86
|
+
>
|
|
87
|
+
<View className="flex-row items-center gap-2">
|
|
88
|
+
<IconTool size={largerText ? 20 : 16} color={tokens.color.muted[scheme]} />
|
|
89
|
+
<Text
|
|
90
|
+
className={cn(caption, "font-instrument-sans-semibold")}
|
|
91
|
+
style={[sizeStyle(caption), typeStyle("semibold"), textStyle("muted", scheme)]}
|
|
92
|
+
>
|
|
93
|
+
{pending ? "Sending decision…" : heading}
|
|
94
|
+
</Text>
|
|
95
|
+
</View>
|
|
96
|
+
<Text
|
|
97
|
+
testID={`${testID}-name`}
|
|
98
|
+
numberOfLines={2}
|
|
99
|
+
className={cn(body, "font-instrument-sans-semibold")}
|
|
100
|
+
style={[sizeStyle(body), typeStyle("semibold"), textStyle("foreground", scheme)]}
|
|
101
|
+
>
|
|
102
|
+
{toolName}
|
|
103
|
+
</Text>
|
|
104
|
+
{summary ? (
|
|
105
|
+
<Text
|
|
106
|
+
testID={`${testID}-args`}
|
|
107
|
+
numberOfLines={3}
|
|
108
|
+
className={small}
|
|
109
|
+
style={[sizeStyle(small), textStyle("muted", scheme), { fontFamily: MONO_FONT }]}
|
|
110
|
+
>
|
|
111
|
+
{summary}
|
|
112
|
+
</Text>
|
|
113
|
+
) : null}
|
|
114
|
+
<View className="flex-row flex-wrap items-center justify-end gap-2" accessibilityState={{ busy: pending }}>
|
|
115
|
+
<CardButton
|
|
116
|
+
testID={`${testID}-deny`}
|
|
117
|
+
label={denyLabel}
|
|
118
|
+
accessibilityLabel={`${denyLabel} ${toolName}`}
|
|
119
|
+
variant="outline"
|
|
120
|
+
disabled={off}
|
|
121
|
+
onPress={() => onDecide?.("deny")}
|
|
122
|
+
/>
|
|
123
|
+
{allowEdit ? (
|
|
124
|
+
<CardButton
|
|
125
|
+
testID={`${testID}-edit`}
|
|
126
|
+
label={editLabel}
|
|
127
|
+
accessibilityLabel={`${editLabel} ${toolName}`}
|
|
128
|
+
variant="secondary"
|
|
129
|
+
disabled={off}
|
|
130
|
+
onPress={() => onDecide?.("edit")}
|
|
131
|
+
/>
|
|
132
|
+
) : null}
|
|
133
|
+
<CardButton
|
|
134
|
+
testID={`${testID}-approve`}
|
|
135
|
+
label={approveLabel}
|
|
136
|
+
accessibilityLabel={`${approveLabel} ${toolName}`}
|
|
137
|
+
variant="primary"
|
|
138
|
+
disabled={off}
|
|
139
|
+
onPress={() => onDecide?.("approve")}
|
|
140
|
+
/>
|
|
141
|
+
</View>
|
|
142
|
+
</View>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* A pending tool call that needs a yes or no: the tool name, a cut summary of
|
|
148
|
+
* the arguments, and Deny and Approve buttons (44 point targets). With
|
|
149
|
+
* `pending` or `disabled` both buttons are off.
|
|
150
|
+
*/
|
|
151
|
+
export function ToolApprovalCard(props: ToolApprovalCardProps) {
|
|
152
|
+
return <ToolCardBase {...props} heading="Approval needed" />;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export type ToolDraftCardProps = ToolApprovalCardProps & {
|
|
156
|
+
editLabel?: string;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/** As `ToolApprovalCard`, plus an Edit button that reports `"edit"`, for a draft the reader may change first. */
|
|
160
|
+
export function ToolDraftCard({ testID = "tool-draft-card", ...props }: ToolDraftCardProps) {
|
|
161
|
+
return <ToolCardBase {...props} testID={testID} allowEdit heading="Draft for review" />;
|
|
162
|
+
}
|
package/src/ai/trace.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import { createContext, useContext, type ComponentType, type ReactNode } from "r
|
|
|
2
2
|
import { Pressable, Text, View, type ViewProps } from "react-native";
|
|
3
3
|
import type { IconProps } from "@tabler/icons-react-native";
|
|
4
4
|
import { cn } from "../cn";
|
|
5
|
-
import { sizeStyle
|
|
5
|
+
import { sizeStyle } from "../controlStyles";
|
|
6
6
|
import { palette } from "../palette";
|
|
7
7
|
import { useLargerText } from "../theme";
|
|
8
8
|
import { tokens } from "../tokens";
|
|
@@ -40,9 +40,13 @@ function useTraceColor() {
|
|
|
40
40
|
return useContext(TraceLineColorContext) ?? tokens.color.muted[scheme];
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* A column of trace lines (web `flex w-full flex-col gap-2`). Each line is 28
|
|
45
|
+
* high (its `py-1` around the 20 text) with no gap, so the rows keep the web
|
|
46
|
+
* 28 pitch and each is its whole tap area, without slop over its neighbours.
|
|
47
|
+
*/
|
|
44
48
|
export function AgentTrace({ className, ...props }: ViewProps & { className?: string }) {
|
|
45
|
-
return <View className={cn("w-full min-w-0
|
|
49
|
+
return <View className={cn("w-full min-w-0", className)} {...props} />;
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
export type AgentTraceLineProps = ViewProps & {
|
|
@@ -50,13 +54,13 @@ export type AgentTraceLineProps = ViewProps & {
|
|
|
50
54
|
className?: string;
|
|
51
55
|
};
|
|
52
56
|
|
|
53
|
-
/** One line: a row of icon, title and description. `pending` fades it to 60%, `error` turns it the danger colour. */
|
|
57
|
+
/** One line, 28 high (`py-1`): a row of icon, title and description. `pending` fades it to 60%, `error` turns it the danger colour. */
|
|
54
58
|
export function AgentTraceLine({ status, className, style, ...props }: AgentTraceLineProps) {
|
|
55
59
|
const scheme = useScheme();
|
|
56
60
|
return (
|
|
57
61
|
<TraceLineColorContext.Provider value={traceLineColor(status, scheme)}>
|
|
58
62
|
<View
|
|
59
|
-
className={cn("min-w-0 max-w-full shrink flex-row items-center gap-2", className)}
|
|
63
|
+
className={cn("min-w-0 max-w-full shrink flex-row items-center gap-2 py-1", className)}
|
|
60
64
|
style={[status === "pending" ? { opacity: 0.6 } : null, style]}
|
|
61
65
|
{...props}
|
|
62
66
|
/>
|
|
@@ -149,7 +153,17 @@ export type AgentTraceCollapsibleTriggerProps = CollapsibleTriggerProps & {
|
|
|
149
153
|
trailing?: ReactNode;
|
|
150
154
|
};
|
|
151
155
|
|
|
152
|
-
/**
|
|
156
|
+
/** No slop: the 28 high line is the whole target, as trace lines stack with no gap. */
|
|
157
|
+
const LINE_SLOP = { top: 0, bottom: 0, left: 0, right: 0 } as const;
|
|
158
|
+
|
|
159
|
+
/** The chevron's slop: 4 back toward the line, 14 on past it (a 34 wide target), none above or below. */
|
|
160
|
+
const CHEVRON_SLOP = { top: 0, bottom: 0, left: 4, right: 14 } as const;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The line as a toggle, with a chevron after it that turns down when open.
|
|
164
|
+
* The trigger stretches to the line's 28 height and takes no slop above or
|
|
165
|
+
* below, so a tap under one line never opens the next.
|
|
166
|
+
*/
|
|
153
167
|
export function AgentTraceCollapsibleTrigger({
|
|
154
168
|
children,
|
|
155
169
|
trailing,
|
|
@@ -166,6 +180,7 @@ export function AgentTraceCollapsibleTrigger({
|
|
|
166
180
|
if (trailing === undefined) {
|
|
167
181
|
return (
|
|
168
182
|
<CollapsibleTrigger
|
|
183
|
+
hitSlop={LINE_SLOP}
|
|
169
184
|
className={cn("max-w-full flex-row items-center gap-1 self-start", className)}
|
|
170
185
|
{...props}
|
|
171
186
|
>
|
|
@@ -176,7 +191,7 @@ export function AgentTraceCollapsibleTrigger({
|
|
|
176
191
|
}
|
|
177
192
|
return (
|
|
178
193
|
<View className={cn("max-w-full flex-row items-center gap-2 self-start", className)}>
|
|
179
|
-
<CollapsibleTrigger className="min-w-0 shrink flex-row items-center" {...props}>
|
|
194
|
+
<CollapsibleTrigger hitSlop={LINE_SLOP} className="min-w-0 shrink flex-row items-center self-stretch" {...props}>
|
|
180
195
|
{children}
|
|
181
196
|
</CollapsibleTrigger>
|
|
182
197
|
{trailing}
|
|
@@ -184,8 +199,9 @@ export function AgentTraceCollapsibleTrigger({
|
|
|
184
199
|
<Pressable
|
|
185
200
|
accessible={false}
|
|
186
201
|
importantForAccessibility="no-hide-descendants"
|
|
187
|
-
hitSlop={
|
|
202
|
+
hitSlop={CHEVRON_SLOP}
|
|
188
203
|
onPress={() => setOpen(!open)}
|
|
204
|
+
className="self-stretch justify-center"
|
|
189
205
|
>
|
|
190
206
|
{chevron}
|
|
191
207
|
</Pressable>
|
|
@@ -210,7 +226,7 @@ export function AgentTraceCollapsibleContent({
|
|
|
210
226
|
return (
|
|
211
227
|
<CollapsibleContent>
|
|
212
228
|
<View
|
|
213
|
-
className={cn("mb-
|
|
229
|
+
className={cn("mb-2 mt-1 min-w-0 pl-4 pr-1", className)}
|
|
214
230
|
style={[
|
|
215
231
|
{
|
|
216
232
|
marginLeft: iconSize / 2 - 1,
|
|
@@ -234,31 +250,52 @@ export function AgentTraceCollapsibleContent({
|
|
|
234
250
|
}
|
|
235
251
|
|
|
236
252
|
export type AgentTraceFoldProps = Omit<CollapsibleProps, "children"> & {
|
|
237
|
-
/** How long the turn ran, in seconds. The title reads "Worked for 1m 5s". */
|
|
238
|
-
seconds: number;
|
|
239
253
|
/** The settled turn's steps, shown in an `AgentTrace` when open. */
|
|
240
254
|
children: ReactNode;
|
|
241
|
-
}
|
|
255
|
+
} & (
|
|
256
|
+
| {
|
|
257
|
+
/** How long the turn ran, in seconds. The title reads "Worked for 1m 5s" (`formatElapsed`). */
|
|
258
|
+
seconds: number;
|
|
259
|
+
duration?: never;
|
|
260
|
+
}
|
|
261
|
+
| {
|
|
262
|
+
/**
|
|
263
|
+
* The duration already formatted, as the web `ActivityTasks` `duration`:
|
|
264
|
+
* the title reads "Worked for " and this. For an app's own formatter.
|
|
265
|
+
*/
|
|
266
|
+
duration: string;
|
|
267
|
+
seconds?: never;
|
|
268
|
+
}
|
|
269
|
+
);
|
|
242
270
|
|
|
243
271
|
/**
|
|
244
272
|
* A settled turn folded behind one line, as the web `ActivityTasks` in its
|
|
245
273
|
* folded (Codex-style) mode: "Worked for …" over a hairline
|
|
246
|
-
* (`border-b border-border pb-1.5
|
|
274
|
+
* 6 below the text (web `border-b border-border pb-1.5`: the line's own 4 and
|
|
275
|
+
* 2 more), closed by default, and the steps in an
|
|
247
276
|
* `AgentTrace` 8 below when open.
|
|
248
277
|
*/
|
|
249
|
-
export function AgentTraceFold({
|
|
278
|
+
export function AgentTraceFold({
|
|
279
|
+
seconds,
|
|
280
|
+
duration,
|
|
281
|
+
children,
|
|
282
|
+
defaultOpen = false,
|
|
283
|
+
className,
|
|
284
|
+
...props
|
|
285
|
+
}: AgentTraceFoldProps) {
|
|
250
286
|
const scheme = useScheme();
|
|
287
|
+
const elapsed = duration ?? formatElapsed(seconds ?? 0);
|
|
251
288
|
return (
|
|
252
289
|
<Collapsible className={cn("w-full min-w-0", className)} defaultOpen={defaultOpen} {...props}>
|
|
253
|
-
<View style={{ borderBottomWidth: 1, borderBottomColor: tokens.color.border[scheme], paddingBottom:
|
|
290
|
+
<View style={{ borderBottomWidth: 1, borderBottomColor: tokens.color.border[scheme], paddingBottom: 2 }}>
|
|
254
291
|
<AgentTraceCollapsibleTrigger>
|
|
255
292
|
<AgentTraceLine>
|
|
256
|
-
<AgentTraceTitle>{`Worked for ${
|
|
293
|
+
<AgentTraceTitle>{`Worked for ${elapsed}`}</AgentTraceTitle>
|
|
257
294
|
</AgentTraceLine>
|
|
258
295
|
</AgentTraceCollapsibleTrigger>
|
|
259
296
|
</View>
|
|
260
297
|
<CollapsibleContent>
|
|
261
|
-
<AgentTrace className="mt-
|
|
298
|
+
<AgentTrace className="mt-1">{children}</AgentTrace>
|
|
262
299
|
</CollapsibleContent>
|
|
263
300
|
</Collapsible>
|
|
264
301
|
);
|