@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,107 @@
|
|
|
1
|
+
// Pure step types for the activity timeline: no React, so `@vvedantb/native-ui`
|
|
2
|
+
// keeps a byte-identical copy (`src/ai/activity-step-types.ts`), with the step
|
|
3
|
+
// labels and grouping that read them.
|
|
4
|
+
|
|
5
|
+
/** One item in a todo checklist step (type "todos"). */
|
|
6
|
+
export interface ActivityTodoItem {
|
|
7
|
+
content: string;
|
|
8
|
+
status: "pending" | "in_progress" | "completed";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ActivityStepOutput {
|
|
12
|
+
text: string;
|
|
13
|
+
exitCode?: number;
|
|
14
|
+
truncated?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ActivityStepEdit {
|
|
18
|
+
oldText: string;
|
|
19
|
+
newText: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** One choice offered by an AskUserQuestion prompt. */
|
|
23
|
+
export interface ActivityQuestionOption {
|
|
24
|
+
label: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One question of an AskUserQuestion prompt, with the choices shown. */
|
|
29
|
+
export interface ActivityQuestion {
|
|
30
|
+
question: string;
|
|
31
|
+
header?: string;
|
|
32
|
+
multiSelect?: boolean;
|
|
33
|
+
options: ActivityQuestionOption[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ActivityStep {
|
|
37
|
+
type:
|
|
38
|
+
| "read"
|
|
39
|
+
| "edit"
|
|
40
|
+
| "write"
|
|
41
|
+
| "bash"
|
|
42
|
+
| "search_files"
|
|
43
|
+
| "search_code"
|
|
44
|
+
| "web_fetch"
|
|
45
|
+
| "web_search"
|
|
46
|
+
| "subtask"
|
|
47
|
+
| "notebook"
|
|
48
|
+
| "thinking"
|
|
49
|
+
| "reasoning"
|
|
50
|
+
| "response"
|
|
51
|
+
| "question"
|
|
52
|
+
| "todos"
|
|
53
|
+
| "tool"
|
|
54
|
+
| "notice"
|
|
55
|
+
| "hook"
|
|
56
|
+
| "status"
|
|
57
|
+
/** A queued user message the agent took into this live turn (Steer). */
|
|
58
|
+
| "steer";
|
|
59
|
+
label: string;
|
|
60
|
+
detail?: string;
|
|
61
|
+
/** Full, unshortened path for file-type steps. Powers the chat File Viewer. */
|
|
62
|
+
path?: string;
|
|
63
|
+
/** `pending`: a planned step that has not started (sandbox startup stages). */
|
|
64
|
+
status: "complete" | "active" | "pending";
|
|
65
|
+
/** The tool_use id that produced this step (Claude only). */
|
|
66
|
+
toolUseId?: string;
|
|
67
|
+
/** Parent `Agent` tool_use id — set on steps that ran inside a subagent. */
|
|
68
|
+
parentToolUseId?: string;
|
|
69
|
+
/** Todo checklist snapshot (type "todos" only). */
|
|
70
|
+
todos?: ActivityTodoItem[];
|
|
71
|
+
/** Bash command (fuller than detail, capped). */
|
|
72
|
+
command?: string;
|
|
73
|
+
/** Tool result transcript (tail-capped). */
|
|
74
|
+
output?: ActivityStepOutput;
|
|
75
|
+
/** Edit before/after snippets. */
|
|
76
|
+
edits?: ActivityStepEdit[];
|
|
77
|
+
/** Codex file_change paths. */
|
|
78
|
+
files?: string[];
|
|
79
|
+
/** Write tool content head preview. */
|
|
80
|
+
contentPreview?: string;
|
|
81
|
+
/** True when the tool failed or exited non-zero. */
|
|
82
|
+
isError?: boolean;
|
|
83
|
+
/** Wall time from push → complete (ms). */
|
|
84
|
+
durationMs?: number;
|
|
85
|
+
/** AskUserQuestion prompt (type "question" only): the questions and options shown to the user. */
|
|
86
|
+
questions?: ActivityQuestion[];
|
|
87
|
+
/** AskUserQuestion answers keyed by question text. Absent when the turn ended without a structured answer. */
|
|
88
|
+
answers?: Record<string, string>;
|
|
89
|
+
/**
|
|
90
|
+
* The transcript query stripped this step's expanded-only fields (`output`,
|
|
91
|
+
* `edits`, `contentPreview`) to keep the chat subscription small. The row
|
|
92
|
+
* still opens; the body fills in once the full payload is fetched.
|
|
93
|
+
*/
|
|
94
|
+
hasHiddenDetail?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** True when the step has expandable rich detail to show. */
|
|
98
|
+
export function activityStepHasRichDetail(step: ActivityStep): boolean {
|
|
99
|
+
return Boolean(
|
|
100
|
+
step.command ||
|
|
101
|
+
step.output ||
|
|
102
|
+
(step.edits && step.edits.length > 0) ||
|
|
103
|
+
(step.files && step.files.length > 0) ||
|
|
104
|
+
step.contentPreview ||
|
|
105
|
+
step.hasHiddenDetail,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { ActivityStep } from "./activity-step-types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One row in the activity timeline. Subtask rows may nest child rows that ran
|
|
5
|
+
* inside that agent (keyed by parentToolUseId → toolUseId).
|
|
6
|
+
*/
|
|
7
|
+
export interface ActivityRow {
|
|
8
|
+
step: ActivityStep;
|
|
9
|
+
children?: ActivityRow[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Status-filler thinking labels emitted by older callback versions (and by
|
|
14
|
+
* prod until the current callback deploys). The response/reasoning text is
|
|
15
|
+
* shown directly, so these narration rows are pure noise — hide them.
|
|
16
|
+
*/
|
|
17
|
+
const LEGACY_NOISE_LABELS = new Set([
|
|
18
|
+
"Generating response...",
|
|
19
|
+
"Generated response",
|
|
20
|
+
"Streaming response...",
|
|
21
|
+
"Streamed response",
|
|
22
|
+
"Finalizing response...",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
/** Thinking labels that mark model reasoning rather than status narration. */
|
|
26
|
+
const REASONING_LABELS = new Set(["Thinking...", "Thought"]);
|
|
27
|
+
|
|
28
|
+
/** Placeholder details older callbacks attached to reasoning-label steps. */
|
|
29
|
+
const FILLER_DETAILS = new Set([
|
|
30
|
+
"Claude is reasoning...",
|
|
31
|
+
"Codex is reasoning...",
|
|
32
|
+
"Opencode is reasoning...",
|
|
33
|
+
"Cursor is reasoning...",
|
|
34
|
+
"Receiving reply...",
|
|
35
|
+
]);
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Older callbacks stored reasoning as thinking steps labelled
|
|
39
|
+
* "Thinking..."/"Thought". Remap those with real text to reasoning blocks;
|
|
40
|
+
* drop the bare placeholder ones (nothing to show).
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeStep(step: ActivityStep): ActivityStep | null {
|
|
43
|
+
if (step.type !== "thinking") return step;
|
|
44
|
+
if (LEGACY_NOISE_LABELS.has(step.label)) return null;
|
|
45
|
+
if (REASONING_LABELS.has(step.label)) {
|
|
46
|
+
if (step.detail && !FILLER_DETAILS.has(step.detail)) {
|
|
47
|
+
return { ...step, type: "reasoning" };
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return step;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Builds one row per normalized step (no consecutive-type merging). */
|
|
55
|
+
function stepsToRows(steps: ActivityStep[]): ActivityRow[] {
|
|
56
|
+
const rows: ActivityRow[] = [];
|
|
57
|
+
for (const raw of steps) {
|
|
58
|
+
const step = normalizeStep(raw);
|
|
59
|
+
if (!step) continue;
|
|
60
|
+
rows.push({ step });
|
|
61
|
+
}
|
|
62
|
+
return rows;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Row types that read as narration rather than work: they already summarise
|
|
67
|
+
* themselves, so folding them into an action group would hide their only text.
|
|
68
|
+
*/
|
|
69
|
+
const STANDALONE_TYPES = new Set<ActivityStep["type"]>([
|
|
70
|
+
"todos",
|
|
71
|
+
"notice",
|
|
72
|
+
"status",
|
|
73
|
+
"question",
|
|
74
|
+
"steer",
|
|
75
|
+
]);
|
|
76
|
+
|
|
77
|
+
/** One block in the activity timeline: prose, a lone row, or a folded run. */
|
|
78
|
+
export type ActivitySegment =
|
|
79
|
+
| { kind: "reasoning"; step: ActivityStep }
|
|
80
|
+
| { kind: "response"; step: ActivityStep }
|
|
81
|
+
| { kind: "row"; row: ActivityRow }
|
|
82
|
+
| { kind: "actions"; rows: ActivityRow[] };
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Folds rows into the read-then-act rhythm of the turn: each reasoning block
|
|
86
|
+
* stays prose, and the run of tool calls it produced collapses behind a single
|
|
87
|
+
* summary line. Without this the whole turn is one undifferentiated wall of
|
|
88
|
+
* per-call rows and the thinking that motivated them is lost in it.
|
|
89
|
+
*/
|
|
90
|
+
export function groupActivityRows(rows: ActivityRow[]): ActivitySegment[] {
|
|
91
|
+
const segments: ActivitySegment[] = [];
|
|
92
|
+
|
|
93
|
+
for (const row of rows) {
|
|
94
|
+
if (row.step.type === "reasoning") {
|
|
95
|
+
segments.push({ kind: "reasoning", step: row.step });
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
// Mid-turn reply text: it ends the tool run before it, like reasoning.
|
|
99
|
+
if (row.step.type === "response") {
|
|
100
|
+
segments.push({ kind: "response", step: row.step });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (STANDALONE_TYPES.has(row.step.type)) {
|
|
104
|
+
segments.push({ kind: "row", row });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const last = segments.at(-1);
|
|
108
|
+
if (last?.kind === "actions") {
|
|
109
|
+
last.rows.push(row);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
segments.push({ kind: "actions", rows: [row] });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return segments;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* React key for one activity step. `label` and `status` are omitted so a
|
|
120
|
+
* streaming update (Running → Ran, error → recovered) keeps the same instance.
|
|
121
|
+
* `toolUseId` is the identity the callback already stamps; the fallback is
|
|
122
|
+
* type + path/command + occurrence in the list being keyed (never the visible
|
|
123
|
+
* overflow slice — callers must pass the full-list index).
|
|
124
|
+
*/
|
|
125
|
+
export function activityStepKey(
|
|
126
|
+
step: ActivityStep,
|
|
127
|
+
occurrence: number,
|
|
128
|
+
): string {
|
|
129
|
+
const id = step.toolUseId;
|
|
130
|
+
if (id !== undefined && id.length > 0) {
|
|
131
|
+
return `id:${id}`;
|
|
132
|
+
}
|
|
133
|
+
const stable = step.path ?? step.command ?? "";
|
|
134
|
+
return `fb:${step.type}:${stable}:${occurrence}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function activityRowKey(row: ActivityRow, occurrence: number): string {
|
|
138
|
+
return activityStepKey(row.step, occurrence);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** React key for a timeline segment. Action groups key off the first row. */
|
|
142
|
+
export function activitySegmentKey(
|
|
143
|
+
segment: ActivitySegment,
|
|
144
|
+
occurrence: number,
|
|
145
|
+
): string {
|
|
146
|
+
if (segment.kind === "reasoning" || segment.kind === "response") {
|
|
147
|
+
return `${segment.kind}:${activityStepKey(segment.step, occurrence)}`;
|
|
148
|
+
}
|
|
149
|
+
if (segment.kind === "row") {
|
|
150
|
+
return `row:${activityRowKey(segment.row, occurrence)}`;
|
|
151
|
+
}
|
|
152
|
+
const first = segment.rows[0];
|
|
153
|
+
if (first === undefined) {
|
|
154
|
+
return `actions:${occurrence}`;
|
|
155
|
+
}
|
|
156
|
+
return `actions:${activityRowKey(first, occurrence)}`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Builds per-call activity rows. Steps with `parentToolUseId` nest under the
|
|
161
|
+
* matching subtask `toolUseId`. Orphans whose parent was never found append
|
|
162
|
+
* at the top level (never drop).
|
|
163
|
+
*/
|
|
164
|
+
export function buildActivityRows(steps: ActivityStep[]): ActivityRow[] {
|
|
165
|
+
const childrenByParent = new Map<string, ActivityStep[]>();
|
|
166
|
+
const topLevel: ActivityStep[] = [];
|
|
167
|
+
|
|
168
|
+
for (const step of steps) {
|
|
169
|
+
const parentId = step.parentToolUseId;
|
|
170
|
+
if (parentId) {
|
|
171
|
+
const existing = childrenByParent.get(parentId);
|
|
172
|
+
if (existing) existing.push(step);
|
|
173
|
+
else childrenByParent.set(parentId, [step]);
|
|
174
|
+
} else {
|
|
175
|
+
topLevel.push(step);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const rows = stepsToRows(topLevel);
|
|
180
|
+
const attached = new Set<string>();
|
|
181
|
+
|
|
182
|
+
for (const row of rows) {
|
|
183
|
+
if (row.step.type !== "subtask") continue;
|
|
184
|
+
const toolUseId = row.step.toolUseId;
|
|
185
|
+
if (!toolUseId) continue;
|
|
186
|
+
const kids = childrenByParent.get(toolUseId);
|
|
187
|
+
if (kids && kids.length > 0) {
|
|
188
|
+
row.children = stepsToRows(kids);
|
|
189
|
+
attached.add(toolUseId);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
for (const [parentId, kids] of childrenByParent) {
|
|
194
|
+
if (attached.has(parentId)) continue;
|
|
195
|
+
rows.push(...stepsToRows(kids));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return rows;
|
|
199
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// The folded (Codex-style) rules of the web `ActivityTasks`
|
|
2
|
+
// (`@vvedantb/ui` `activity-tasks.tsx`), as pure logic for an app that draws
|
|
3
|
+
// the rows with the `AgentTrace` parts. The row text, the grouping and the
|
|
4
|
+
// step types are byte-identical copies of the web files
|
|
5
|
+
// (`activity-step-label.ts`, `activity-tasks-utils.ts`, `activity-step-types.ts`).
|
|
6
|
+
import type { ActivityRow, ActivitySegment } from "./activity-tasks-utils";
|
|
7
|
+
import { buildActivityRows, groupActivityRows } from "./activity-tasks-utils";
|
|
8
|
+
import type { ActivityStep } from "./activity-step-types";
|
|
9
|
+
|
|
10
|
+
/** Blocks a settled timeline shows before "Show N more" (web `MAX_VISIBLE_ROWS`). */
|
|
11
|
+
export const ACTIVITY_MAX_VISIBLE_BLOCKS = 8;
|
|
12
|
+
|
|
13
|
+
/** Child rows a subagent step shows before "+N more" (web `MAX_VISIBLE_CHILDREN`). */
|
|
14
|
+
export const ACTIVITY_MAX_VISIBLE_CHILDREN = 8;
|
|
15
|
+
|
|
16
|
+
/** Step types the folded layout hides: the reply text and the status thinking lines. */
|
|
17
|
+
const HIDDEN_TYPES = new Set<ActivityStep["type"]>(["thinking", "response"]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A turn's steps as the blocks of the folded timeline: each reasoning step as
|
|
21
|
+
* prose, each run of tool calls behind one summary line, narration (to-dos,
|
|
22
|
+
* notices, questions) as lone rows, and the steps a subagent ran nested under
|
|
23
|
+
* its row. Reply text and status thinking lines are dropped, as on the web.
|
|
24
|
+
*/
|
|
25
|
+
export function foldedActivitySegments(steps: ActivityStep[]): ActivitySegment[] {
|
|
26
|
+
return groupActivityRows(buildActivityRows(steps).filter((row) => !HIDDEN_TYPES.has(row.step.type)));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The child rows a subagent row shows (its reply text belongs to its own
|
|
31
|
+
* thread, so it is dropped), capped at `ACTIVITY_MAX_VISIBLE_CHILDREN`, and
|
|
32
|
+
* how many are left for "+N more".
|
|
33
|
+
*/
|
|
34
|
+
export function activityChildRows(row: ActivityRow): { shown: ActivityRow[]; total: number; overflow: number } {
|
|
35
|
+
const children = (row.children ?? []).filter((child) => !HIDDEN_TYPES.has(child.step.type));
|
|
36
|
+
const overflow = Math.max(0, children.length - ACTIVITY_MAX_VISIBLE_CHILDREN);
|
|
37
|
+
return { shown: children.slice(0, ACTIVITY_MAX_VISIBLE_CHILDREN), total: children.length, overflow };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Whole seconds a finished step took, for the faded "4s" after its row; undefined when unknown or under 1s. */
|
|
41
|
+
export function activityStepSeconds(step: ActivityStep): number | undefined {
|
|
42
|
+
if (step.status !== "complete" || !step.durationMs) return undefined;
|
|
43
|
+
const seconds = Math.round(step.durationMs / 1000);
|
|
44
|
+
return seconds >= 1 ? seconds : undefined;
|
|
45
|
+
}
|
package/src/ai/attachments.tsx
CHANGED
|
@@ -175,7 +175,9 @@ export function Attachment({ data, onRemove, onPress, accessibilityLabel, testID
|
|
|
175
175
|
<Pressable
|
|
176
176
|
accessibilityRole="button"
|
|
177
177
|
accessibilityLabel={`Remove ${label}`}
|
|
178
|
-
|
|
178
|
+
// A pressable label sits 6 before it, and the next chip 8 past this
|
|
179
|
+
// chip's edge: the slop stops short of both, so neither steals a tap.
|
|
180
|
+
hitSlop={{ ...touchSlop(REMOVE_SIZE), left: onPress ? 3 : touchSlop(REMOVE_SIZE).left, right: 8 }}
|
|
179
181
|
onPress={onRemove}
|
|
180
182
|
className="items-center justify-center rounded-full active:opacity-70"
|
|
181
183
|
style={{ width: REMOVE_SIZE, height: REMOVE_SIZE }}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Pressable, Text } from "react-native";
|
|
2
|
+
import { cn } from "../cn";
|
|
3
|
+
import { buttonColors, MIN_TOUCH_TARGET, sizeStyle, touchSlop, typeStyle, type ButtonVariant } from "../controlStyles";
|
|
4
|
+
import { useLargerText } from "../theme";
|
|
5
|
+
import { useScheme } from "../useScheme";
|
|
6
|
+
import { chatTextSize } from "./chat-text";
|
|
7
|
+
|
|
8
|
+
export type CardButtonProps = {
|
|
9
|
+
label: string;
|
|
10
|
+
variant: Extract<ButtonVariant, "primary" | "outline" | "secondary" | "destructive" | "ghost">;
|
|
11
|
+
onPress?: () => void;
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
accessibilityLabel?: string;
|
|
14
|
+
testID?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The compact pill button of the tool and subagent cards: at least 44 points
|
|
19
|
+
* high, label in the `small` chat size (steps up with larger text). Internal.
|
|
20
|
+
*/
|
|
21
|
+
export function CardButton({ label, variant, onPress, disabled, accessibilityLabel, testID }: CardButtonProps) {
|
|
22
|
+
const scheme = useScheme();
|
|
23
|
+
const largerText = useLargerText();
|
|
24
|
+
const size = chatTextSize("small", largerText);
|
|
25
|
+
const colors = buttonColors(variant, scheme)!;
|
|
26
|
+
return (
|
|
27
|
+
<Pressable
|
|
28
|
+
testID={testID}
|
|
29
|
+
accessibilityRole="button"
|
|
30
|
+
accessibilityLabel={accessibilityLabel ?? label}
|
|
31
|
+
accessibilityState={{ disabled: !!disabled }}
|
|
32
|
+
disabled={disabled}
|
|
33
|
+
onPress={onPress}
|
|
34
|
+
hitSlop={touchSlop(MIN_TOUCH_TARGET)}
|
|
35
|
+
className={cn("items-center justify-center rounded-full border px-4 active:opacity-70")}
|
|
36
|
+
style={{
|
|
37
|
+
minHeight: MIN_TOUCH_TARGET,
|
|
38
|
+
minWidth: MIN_TOUCH_TARGET,
|
|
39
|
+
backgroundColor: colors.backgroundColor,
|
|
40
|
+
borderColor: colors.borderColor,
|
|
41
|
+
opacity: disabled ? 0.5 : 1,
|
|
42
|
+
}}
|
|
43
|
+
>
|
|
44
|
+
<Text className={cn(size, "font-instrument-sans-semibold")} style={[sizeStyle(size), typeStyle("semibold"), { color: colors.color }]}>
|
|
45
|
+
{label}
|
|
46
|
+
</Text>
|
|
47
|
+
</Pressable>
|
|
48
|
+
);
|
|
49
|
+
}
|
package/src/ai/chat-text.ts
CHANGED
|
@@ -5,19 +5,25 @@ import type { TextSize } from "../controlStyles";
|
|
|
5
5
|
export type ChatTextKind = "body" | "small" | "caption";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
* `body` is message text: `text-
|
|
9
|
-
* `
|
|
10
|
-
* `
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* text steps each up, as `Textarea` does.
|
|
8
|
+
* `body` is message text: `text-sm` (14 / 20), as the web `MessageContent`.
|
|
9
|
+
* `small` is the secondary text of triggers, trace lines and sources: also
|
|
10
|
+
* `text-sm` (the web sources trigger is `text-xs`). `caption` is `text-xs`
|
|
11
|
+
* (12 / 16): step descriptions, file chips and code headers. Larger text
|
|
12
|
+
* steps each up, as `Textarea` does.
|
|
14
13
|
*/
|
|
15
14
|
export function chatTextSize(kind: ChatTextKind, largerText: boolean): TextSize {
|
|
16
|
-
if (kind === "body") return largerText ? "text-xl" : "text-
|
|
15
|
+
if (kind === "body") return largerText ? "text-xl" : "text-sm";
|
|
17
16
|
if (kind === "small") return largerText ? "text-lg" : "text-sm";
|
|
18
17
|
return largerText ? "text-base" : "text-xs";
|
|
19
18
|
}
|
|
20
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The web `text-2xs` token (11 / 16, tracking 0.01em) as a style, for
|
|
22
|
+
* `MessageMeta size="2xs"` and an app's own small labels. React Native has no
|
|
23
|
+
* `em`, so the tracking is in points.
|
|
24
|
+
*/
|
|
25
|
+
export const META_TEXT_STYLE = { fontSize: 11, lineHeight: 16, letterSpacing: 0.11 } as const;
|
|
26
|
+
|
|
21
27
|
/** The system monospace face, for code and reasoning text (web `font-mono`). */
|
|
22
28
|
export const MONO_FONT = Platform.select({ ios: "Menlo", default: "monospace" });
|
|
23
29
|
|
package/src/ai/chat-thread.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { StyleSheet, View } from "react-native";
|
|
3
3
|
import { cn } from "../cn";
|
|
4
|
+
import { KeyboardAvoidingView, KeyboardLiftContext } from "../primitives/keyboard-parts";
|
|
4
5
|
import {
|
|
5
6
|
Conversation,
|
|
6
7
|
ConversationContent,
|
|
@@ -48,10 +49,10 @@ function lastIndexOfRole(messages: UIMessage[], role: UIMessage["role"]) {
|
|
|
48
49
|
* The chat column as one piece: `Conversation`, `ConversationContent`, the
|
|
49
50
|
* messages, a `thinking` row right after the last user message while
|
|
50
51
|
* `working`, `ConversationScrollButton` and the `footer` last. The whole
|
|
51
|
-
* column sits in a `KeyboardAvoidingView` (`padding`, as `PromptInput
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
52
|
+
* column sits in a `KeyboardAvoidingView` (`padding`, as `PromptInput`; frame
|
|
53
|
+
* by frame with `KeyboardControllerProvider`), which also lifts the footer on
|
|
54
|
+
* Android edge-to-edge, where the window does not resize. A `PromptInput`
|
|
55
|
+
* footer and the scroll button add no lift of their own (`KeyboardLiftContext`).
|
|
55
56
|
*/
|
|
56
57
|
export function ChatThread<M extends UIMessage = UIMessage>({
|
|
57
58
|
messages,
|
|
@@ -69,28 +70,34 @@ export function ChatThread<M extends UIMessage = UIMessage>({
|
|
|
69
70
|
const streaming = working ? lastIndexOfRole(messages, "assistant") : -1;
|
|
70
71
|
|
|
71
72
|
return (
|
|
72
|
-
<KeyboardAvoidingView
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
73
|
+
<KeyboardAvoidingView testID={testID} keyboardVerticalOffset={keyboardVerticalOffset} style={styles.fill}>
|
|
74
|
+
{/* The column lifts as a whole: the parts in it add no lift of their own. */}
|
|
75
|
+
<KeyboardLiftContext.Provider value>
|
|
76
|
+
<View className={cn("flex-1", className)}>
|
|
77
|
+
<Conversation>
|
|
78
|
+
<ConversationContent contentContainerClassName={contentContainerClassName}>
|
|
79
|
+
{messages.length === 0
|
|
80
|
+
? emptyState
|
|
81
|
+
: messages.map((message, index) => (
|
|
82
|
+
<View key={message.id}>
|
|
83
|
+
{renderMessage(message, index === streaming)}
|
|
84
|
+
{index === lastUser && thinking != null ? (
|
|
85
|
+
<View className="mt-4">{thinking}</View>
|
|
86
|
+
) : null}
|
|
87
|
+
</View>
|
|
88
|
+
))}
|
|
89
|
+
</ConversationContent>
|
|
90
|
+
<ConversationScrollButton testID="chat-thread-scroll-bottom" />
|
|
91
|
+
</Conversation>
|
|
92
|
+
{footer}
|
|
93
|
+
</View>
|
|
94
|
+
</KeyboardLiftContext.Provider>
|
|
94
95
|
</KeyboardAvoidingView>
|
|
95
96
|
);
|
|
96
97
|
}
|
|
98
|
+
|
|
99
|
+
const styles = StyleSheet.create({
|
|
100
|
+
fill: {
|
|
101
|
+
flex: 1,
|
|
102
|
+
},
|
|
103
|
+
});
|