bazilion 0.0.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.
Files changed (260) hide show
  1. package/.understand-anything/.understandignore +25 -0
  2. package/.understand-anything/fingerprints.json +14267 -0
  3. package/.understand-anything/knowledge-graph.json +18128 -0
  4. package/.understand-anything/meta.json +6 -0
  5. package/CLAUDE.md +164 -0
  6. package/LICENSE +21 -0
  7. package/README.md +195 -0
  8. package/apps/cli/package.json +21 -0
  9. package/apps/cli/src/auth-file.ts +18 -0
  10. package/apps/cli/src/client.ts +37 -0
  11. package/apps/cli/src/columnize.ts +32 -0
  12. package/apps/cli/src/commands/agent.ts +574 -0
  13. package/apps/cli/src/commands/auth.ts +110 -0
  14. package/apps/cli/src/commands/backup.ts +135 -0
  15. package/apps/cli/src/commands/completion.ts +155 -0
  16. package/apps/cli/src/commands/config.ts +95 -0
  17. package/apps/cli/src/commands/doctor.ts +131 -0
  18. package/apps/cli/src/commands/group.ts +132 -0
  19. package/apps/cli/src/commands/inbox.ts +82 -0
  20. package/apps/cli/src/commands/login.ts +73 -0
  21. package/apps/cli/src/commands/memory.ts +106 -0
  22. package/apps/cli/src/commands/profile.ts +259 -0
  23. package/apps/cli/src/commands/provider.ts +170 -0
  24. package/apps/cli/src/commands/send.ts +24 -0
  25. package/apps/cli/src/commands/serve.ts +89 -0
  26. package/apps/cli/src/commands/skill.ts +120 -0
  27. package/apps/cli/src/commands/token.ts +148 -0
  28. package/apps/cli/src/commands/trigger.ts +129 -0
  29. package/apps/cli/src/commands/uninstall.ts +156 -0
  30. package/apps/cli/src/index.ts +196 -0
  31. package/apps/cli/src/paths.ts +12 -0
  32. package/apps/cli/test/agent.test.ts +213 -0
  33. package/apps/cli/test/backup.test.ts +95 -0
  34. package/apps/cli/test/chat.test.ts +539 -0
  35. package/apps/cli/test/columnize.test.ts +29 -0
  36. package/apps/cli/test/completion.test.ts +45 -0
  37. package/apps/cli/test/config-page.test.ts +151 -0
  38. package/apps/cli/test/group.test.ts +74 -0
  39. package/apps/cli/test/helpers.ts +70 -0
  40. package/apps/cli/test/inbox-autodeliver.test.ts +143 -0
  41. package/apps/cli/test/inbox.test.ts +147 -0
  42. package/apps/cli/test/memory.test.ts +69 -0
  43. package/apps/cli/test/profile.test.ts +110 -0
  44. package/apps/cli/test/send.test.ts +30 -0
  45. package/apps/cli/test/server-fixture.ts +212 -0
  46. package/apps/cli/test/session-head.test.ts +45 -0
  47. package/apps/cli/test/skill.test.ts +128 -0
  48. package/apps/cli/test/token.test.ts +109 -0
  49. package/apps/cli/test/trigger.test.ts +245 -0
  50. package/apps/cli/tsconfig.json +4 -0
  51. package/apps/daemon/package.json +31 -0
  52. package/apps/daemon/src/app.ts +41 -0
  53. package/apps/daemon/src/core/agent/archive.ts +8 -0
  54. package/apps/daemon/src/core/agent/delete.ts +30 -0
  55. package/apps/daemon/src/core/agent/resolve.ts +31 -0
  56. package/apps/daemon/src/core/agent/spawn.ts +95 -0
  57. package/apps/daemon/src/core/agent/unarchive.ts +11 -0
  58. package/apps/daemon/src/core/availableModels.ts +58 -0
  59. package/apps/daemon/src/core/db/client.ts +113 -0
  60. package/apps/daemon/src/core/db/migrate.ts +41 -0
  61. package/apps/daemon/src/core/db/migrations/0001_init.sql +179 -0
  62. package/apps/daemon/src/core/group/delete.ts +21 -0
  63. package/apps/daemon/src/core/group/register.ts +68 -0
  64. package/apps/daemon/src/core/index.ts +71 -0
  65. package/apps/daemon/src/core/paths.ts +56 -0
  66. package/apps/daemon/src/core/profile/create.ts +78 -0
  67. package/apps/daemon/src/core/profile/delete.ts +26 -0
  68. package/apps/daemon/src/core/profile/identity.ts +70 -0
  69. package/apps/daemon/src/core/profile/load.ts +45 -0
  70. package/apps/daemon/src/core/profile/seed.ts +74 -0
  71. package/apps/daemon/src/core/profile/templates.ts +60 -0
  72. package/apps/daemon/src/core/profile/update.ts +57 -0
  73. package/apps/daemon/src/core/profile/validate.ts +9 -0
  74. package/apps/daemon/src/core/repos/agents.ts +202 -0
  75. package/apps/daemon/src/core/repos/config.ts +78 -0
  76. package/apps/daemon/src/core/repos/groups.ts +55 -0
  77. package/apps/daemon/src/core/repos/messages.ts +127 -0
  78. package/apps/daemon/src/core/repos/profiles.ts +83 -0
  79. package/apps/daemon/src/core/repos/providerModels.ts +58 -0
  80. package/apps/daemon/src/core/repos/providerState.ts +37 -0
  81. package/apps/daemon/src/core/repos/secrets.ts +145 -0
  82. package/apps/daemon/src/core/repos/skillMeta.ts +49 -0
  83. package/apps/daemon/src/core/repos/triggers.ts +101 -0
  84. package/apps/daemon/src/core/repos/webTokens.ts +87 -0
  85. package/apps/daemon/src/core/secrets.ts +65 -0
  86. package/apps/daemon/src/core/services.ts +264 -0
  87. package/apps/daemon/src/core/skills/discover.ts +28 -0
  88. package/apps/daemon/src/core/skills/import.ts +136 -0
  89. package/apps/daemon/src/core/skills/parse.ts +52 -0
  90. package/apps/daemon/src/core/skills/resolve.ts +50 -0
  91. package/apps/daemon/src/index.ts +45 -0
  92. package/apps/daemon/src/lib/agent-cancel.ts +48 -0
  93. package/apps/daemon/src/lib/agent-id.ts +13 -0
  94. package/apps/daemon/src/lib/agent-turn.ts +56 -0
  95. package/apps/daemon/src/lib/api-key.ts +56 -0
  96. package/apps/daemon/src/lib/auth.ts +41 -0
  97. package/apps/daemon/src/lib/cron.ts +93 -0
  98. package/apps/daemon/src/lib/ctx.ts +80 -0
  99. package/apps/daemon/src/lib/messaging-host.ts +34 -0
  100. package/apps/daemon/src/lib/middleware-auth.ts +52 -0
  101. package/apps/daemon/src/lib/scheduler.ts +294 -0
  102. package/apps/daemon/src/routes/agents.ts +772 -0
  103. package/apps/daemon/src/routes/auth-login.ts +193 -0
  104. package/apps/daemon/src/routes/config.ts +267 -0
  105. package/apps/daemon/src/routes/groups.ts +133 -0
  106. package/apps/daemon/src/routes/messages.ts +29 -0
  107. package/apps/daemon/src/routes/misc.ts +239 -0
  108. package/apps/daemon/src/routes/profiles.ts +197 -0
  109. package/apps/daemon/src/routes/skills.ts +123 -0
  110. package/apps/daemon/src/routes/triggers.ts +29 -0
  111. package/apps/daemon/src/runtime/auth/openai-codex.ts +121 -0
  112. package/apps/daemon/src/runtime/auto-reply/heartbeat.ts +31 -0
  113. package/apps/daemon/src/runtime/index.ts +77 -0
  114. package/apps/daemon/src/runtime/memory/files.ts +103 -0
  115. package/apps/daemon/src/runtime/memory/qmd.ts +152 -0
  116. package/apps/daemon/src/runtime/memory/types.ts +16 -0
  117. package/apps/daemon/src/runtime/pi/events.ts +173 -0
  118. package/apps/daemon/src/runtime/pi/session.ts +536 -0
  119. package/apps/daemon/src/runtime/pi/tools.ts +85 -0
  120. package/apps/daemon/src/runtime/providers/catalog.ts +145 -0
  121. package/apps/daemon/src/runtime/providers/pi-adapter.ts +272 -0
  122. package/apps/daemon/src/runtime/providers/registry.ts +374 -0
  123. package/apps/daemon/src/runtime/providers/retry.ts +176 -0
  124. package/apps/daemon/src/runtime/providers/types.ts +33 -0
  125. package/apps/daemon/src/runtime/session/prompt.ts +83 -0
  126. package/apps/daemon/src/runtime/tools/bootstrap.ts +22 -0
  127. package/apps/daemon/src/runtime/tools/home.ts +114 -0
  128. package/apps/daemon/src/runtime/tools/memory.ts +81 -0
  129. package/apps/daemon/src/runtime/tools/messaging.ts +127 -0
  130. package/apps/daemon/src/runtime/tools/registry.ts +29 -0
  131. package/apps/daemon/src/runtime/tools/types.ts +13 -0
  132. package/apps/daemon/src/runtime/tools/web-extract.ts +110 -0
  133. package/apps/daemon/src/runtime/tools/web-ssrf.ts +245 -0
  134. package/apps/daemon/src/runtime/tools/web.ts +273 -0
  135. package/apps/daemon/src/runtime/worker/entry.ts +221 -0
  136. package/apps/daemon/src/runtime/worker/ipc-protocol.ts +75 -0
  137. package/apps/daemon/src/runtime/worker/spawn.ts +249 -0
  138. package/apps/daemon/test/core/agents.test.ts +319 -0
  139. package/apps/daemon/test/core/available-models.test.ts +63 -0
  140. package/apps/daemon/test/core/config.test.ts +99 -0
  141. package/apps/daemon/test/core/groups.test.ts +63 -0
  142. package/apps/daemon/test/core/helpers.ts +50 -0
  143. package/apps/daemon/test/core/identity.test.ts +85 -0
  144. package/apps/daemon/test/core/migrations.test.ts +83 -0
  145. package/apps/daemon/test/core/profiles.test.ts +182 -0
  146. package/apps/daemon/test/core/provider-models.test.ts +57 -0
  147. package/apps/daemon/test/core/provider-state.test.ts +36 -0
  148. package/apps/daemon/test/core/skill-meta.test.ts +45 -0
  149. package/apps/daemon/test/core/skills.test.ts +271 -0
  150. package/apps/daemon/test/core/triggers.test.ts +182 -0
  151. package/apps/daemon/test/core/web-tokens.test.ts +79 -0
  152. package/apps/daemon/test/cron.test.ts +90 -0
  153. package/apps/daemon/test/runtime/heartbeat.test.ts +34 -0
  154. package/apps/daemon/test/runtime/memory-qmd.test.ts +97 -0
  155. package/apps/daemon/test/runtime/memory.test.ts +78 -0
  156. package/apps/daemon/test/runtime/messaging.test.ts +213 -0
  157. package/apps/daemon/test/runtime/mock-server.ts +65 -0
  158. package/apps/daemon/test/runtime/openai-codex-auth.test.ts +116 -0
  159. package/apps/daemon/test/runtime/providers.test.ts +306 -0
  160. package/apps/daemon/test/runtime/retry.test.ts +191 -0
  161. package/apps/daemon/test/runtime/session-head.test.ts +90 -0
  162. package/apps/daemon/test/runtime/tools-home.test.ts +102 -0
  163. package/apps/daemon/test/runtime/tools-web.test.ts +206 -0
  164. package/apps/daemon/tsconfig.json +4 -0
  165. package/apps/mobile/README.md +60 -0
  166. package/apps/mobile/app/_layout.tsx +58 -0
  167. package/apps/mobile/app/agents/[id]/chat.tsx +486 -0
  168. package/apps/mobile/app/agents/[id]/index.tsx +166 -0
  169. package/apps/mobile/app/agents/index.tsx +212 -0
  170. package/apps/mobile/app/index.tsx +21 -0
  171. package/apps/mobile/app/pair.tsx +226 -0
  172. package/apps/mobile/app/settings.tsx +419 -0
  173. package/apps/mobile/app.json +49 -0
  174. package/apps/mobile/assets/adaptive-icon.png +0 -0
  175. package/apps/mobile/assets/favicon.png +0 -0
  176. package/apps/mobile/assets/icon.png +0 -0
  177. package/apps/mobile/assets/splash-icon.png +0 -0
  178. package/apps/mobile/babel.config.js +6 -0
  179. package/apps/mobile/metro.config.js +28 -0
  180. package/apps/mobile/package.json +44 -0
  181. package/apps/mobile/src/auth.ts +66 -0
  182. package/apps/mobile/src/pair-url.ts +48 -0
  183. package/apps/mobile/src/theme-context.tsx +88 -0
  184. package/apps/mobile/src/theme.ts +135 -0
  185. package/apps/mobile/test/pair-url.test.ts +46 -0
  186. package/apps/mobile/tsconfig.json +23 -0
  187. package/apps/web/components.json +25 -0
  188. package/apps/web/package.json +39 -0
  189. package/apps/web/public/baziu.svg +8 -0
  190. package/apps/web/src/components/AgentTabs.tsx +45 -0
  191. package/apps/web/src/components/BaziuLogo.tsx +21 -0
  192. package/apps/web/src/components/ChatPane.tsx +1033 -0
  193. package/apps/web/src/components/ConfigTabs.tsx +29 -0
  194. package/apps/web/src/components/CopyButton.tsx +68 -0
  195. package/apps/web/src/components/CreateGroupDialog.tsx +127 -0
  196. package/apps/web/src/components/FieldRow.tsx +94 -0
  197. package/apps/web/src/components/Footer.tsx +10 -0
  198. package/apps/web/src/components/PawIcon.tsx +15 -0
  199. package/apps/web/src/components/Sidebar.tsx +287 -0
  200. package/apps/web/src/components/SpawnDialog.tsx +129 -0
  201. package/apps/web/src/components/ThemeToggle.tsx +75 -0
  202. package/apps/web/src/components/TopNav.tsx +34 -0
  203. package/apps/web/src/components/ui/button.tsx +67 -0
  204. package/apps/web/src/components/ui/card.tsx +103 -0
  205. package/apps/web/src/components/ui/checkbox.tsx +31 -0
  206. package/apps/web/src/components/ui/dialog.tsx +168 -0
  207. package/apps/web/src/components/ui/input.tsx +19 -0
  208. package/apps/web/src/components/ui/label.tsx +22 -0
  209. package/apps/web/src/components/ui/radio-group.tsx +44 -0
  210. package/apps/web/src/components/ui/select.tsx +192 -0
  211. package/apps/web/src/components/ui/separator.tsx +26 -0
  212. package/apps/web/src/components/ui/table.tsx +116 -0
  213. package/apps/web/src/components/ui/tabs.tsx +88 -0
  214. package/apps/web/src/components/ui/textarea.tsx +18 -0
  215. package/apps/web/src/lib/auth.ts +50 -0
  216. package/apps/web/src/lib/daemon-client.ts +34 -0
  217. package/apps/web/src/lib/md.ts +45 -0
  218. package/apps/web/src/lib/utils.ts +6 -0
  219. package/apps/web/src/lib/wire-constants.ts +27 -0
  220. package/apps/web/src/routeTree.gen.ts +408 -0
  221. package/apps/web/src/router.tsx +20 -0
  222. package/apps/web/src/routes/__root.tsx +123 -0
  223. package/apps/web/src/routes/agents/$id/inbox.tsx +207 -0
  224. package/apps/web/src/routes/agents/$id/index.tsx +527 -0
  225. package/apps/web/src/routes/agents/$id/triggers.tsx +239 -0
  226. package/apps/web/src/routes/agents/index.tsx +265 -0
  227. package/apps/web/src/routes/api/$.ts +88 -0
  228. package/apps/web/src/routes/config/index.tsx +315 -0
  229. package/apps/web/src/routes/config/services.tsx +49 -0
  230. package/apps/web/src/routes/config/tokens.tsx +192 -0
  231. package/apps/web/src/routes/groups/$id/index.tsx +153 -0
  232. package/apps/web/src/routes/groups/$id/memory.tsx +321 -0
  233. package/apps/web/src/routes/groups/index.tsx +191 -0
  234. package/apps/web/src/routes/index.tsx +133 -0
  235. package/apps/web/src/routes/login.tsx +63 -0
  236. package/apps/web/src/routes/profiles/$id.tsx +549 -0
  237. package/apps/web/src/routes/profiles/index.tsx +458 -0
  238. package/apps/web/src/routes/skills/index.tsx +297 -0
  239. package/apps/web/src/routes/welcome.tsx +61 -0
  240. package/apps/web/src/styles.css +449 -0
  241. package/apps/web/tsconfig.json +25 -0
  242. package/apps/web/vite.config.ts +25 -0
  243. package/biome.json +23 -0
  244. package/docs/agent-engine.md +219 -0
  245. package/docs/architecture.md +627 -0
  246. package/docs/backlog/README.md +42 -0
  247. package/docs/backlog/draft/BAZ-001-a2a-federation-spike.md +125 -0
  248. package/docs/openclaw-reference.md +210 -0
  249. package/package.json +38 -0
  250. package/packages/api-types/package.json +11 -0
  251. package/packages/api-types/src/entities.ts +146 -0
  252. package/packages/api-types/src/events.ts +55 -0
  253. package/packages/api-types/src/index.ts +488 -0
  254. package/packages/api-types/src/memory.ts +15 -0
  255. package/packages/client/package.json +13 -0
  256. package/packages/client/src/index.ts +117 -0
  257. package/pnpm-workspace.yaml +3 -0
  258. package/tsconfig.base.json +23 -0
  259. package/tsconfig.json +11 -0
  260. package/vitest.config.ts +22 -0
@@ -0,0 +1,58 @@
1
+ import { DMSans_400Regular, DMSans_500Medium, DMSans_700Bold } from '@expo-google-fonts/dm-sans'
2
+ import { DMSerifDisplay_400Regular } from '@expo-google-fonts/dm-serif-display'
3
+ import { JetBrainsMono_400Regular } from '@expo-google-fonts/jetbrains-mono'
4
+ import { useFonts } from 'expo-font'
5
+ import { Stack } from 'expo-router'
6
+ import { StatusBar } from 'expo-status-bar'
7
+ import { ThemeProvider, useColors, useResolvedScheme } from '@/src/theme-context'
8
+ import { fonts } from '@/src/theme'
9
+
10
+ export default function RootLayout() {
11
+ // Aliases (BaziuDisplay / BaziuBody / …) are what each StyleSheet
12
+ // references via theme.fonts.* — keys here are the registered family
13
+ // names, values are the asset module IDs imported above.
14
+ const [loaded] = useFonts({
15
+ BaziuDisplay: DMSerifDisplay_400Regular,
16
+ BaziuBody: DMSans_400Regular,
17
+ BaziuBodyMedium: DMSans_500Medium,
18
+ BaziuBodyBold: DMSans_700Bold,
19
+ BaziuMono: JetBrainsMono_400Regular,
20
+ })
21
+
22
+ // Block render until fonts settle so screens never flash with the
23
+ // system fallback font.
24
+ if (!loaded) return null
25
+
26
+ return (
27
+ <ThemeProvider>
28
+ <ThemedStack />
29
+ </ThemeProvider>
30
+ )
31
+ }
32
+
33
+ // Inner component so it can subscribe to the theme. The Stack needs to
34
+ // re-render with new header/background colors when the user toggles.
35
+ function ThemedStack() {
36
+ const colors = useColors()
37
+ const resolved = useResolvedScheme()
38
+ return (
39
+ <>
40
+ <StatusBar style={resolved === 'dark' ? 'light' : 'dark'} />
41
+ <Stack
42
+ screenOptions={{
43
+ headerStyle: { backgroundColor: colors.background },
44
+ headerTintColor: colors.foreground,
45
+ headerTitleStyle: { fontFamily: fonts.display, fontSize: 18 },
46
+ contentStyle: { backgroundColor: colors.background },
47
+ }}
48
+ >
49
+ <Stack.Screen name="index" options={{ headerShown: false }} />
50
+ <Stack.Screen name="pair" options={{ title: 'Pair' }} />
51
+ <Stack.Screen name="agents/index" options={{ title: 'Agents' }} />
52
+ <Stack.Screen name="agents/[id]/index" options={{ title: 'Agent' }} />
53
+ <Stack.Screen name="agents/[id]/chat" options={{ title: 'Chat' }} />
54
+ <Stack.Screen name="settings" options={{ title: 'Settings' }} />
55
+ </Stack>
56
+ </>
57
+ )
58
+ }
@@ -0,0 +1,486 @@
1
+ import type { ChatFrame, ProviderMessage, SessionEvent } from '@bazilion/api-types'
2
+ import { useHeaderHeight } from '@react-navigation/elements'
3
+ import { router, useLocalSearchParams } from 'expo-router'
4
+ import { useCallback, useEffect, useMemo, useState } from 'react'
5
+ import {
6
+ ActivityIndicator,
7
+ FlatList,
8
+ KeyboardAvoidingView,
9
+ Platform,
10
+ Pressable,
11
+ StyleSheet,
12
+ Text,
13
+ TextInput,
14
+ View,
15
+ } from 'react-native'
16
+ import Markdown from 'react-native-markdown-display'
17
+ import { useSafeAreaInsets } from 'react-native-safe-area-context'
18
+ import { clearCredentials, type Credentials, loadCredentials } from '@/src/auth'
19
+ import { useColors } from '@/src/theme-context'
20
+ import { type Colors, fonts, radii } from '@/src/theme'
21
+
22
+ // Display-side rendering item. Derived from the server's ProviderMessage[]
23
+ // (history) and SessionEvent stream (live turn). Tool-call/result pairs are
24
+ // collapsed into a single tool item so the UI doesn't show them as separate
25
+ // rows.
26
+ type Item =
27
+ | { id: string; kind: 'user'; text: string }
28
+ | { id: string; kind: 'assistant'; text: string }
29
+ | { id: string; kind: 'tool'; name: string; result?: string; error?: string }
30
+ | { id: string; kind: 'error'; text: string }
31
+
32
+ let _itemIdSeq = 0
33
+ const nextItemId = (): string => `i${++_itemIdSeq}`
34
+
35
+ /** Turn the server's flattened ProviderMessage[] (history) into display Items. */
36
+ function historyToItems(messages: ProviderMessage[]): Item[] {
37
+ const out: Item[] = []
38
+ for (const m of messages) {
39
+ if (m.role === 'user') {
40
+ out.push({ id: nextItemId(), kind: 'user', text: m.content })
41
+ } else if (m.role === 'assistant') {
42
+ if (m.content) out.push({ id: nextItemId(), kind: 'assistant', text: m.content })
43
+ for (const tc of m.toolCalls ?? []) {
44
+ out.push({ id: tc.id, kind: 'tool', name: tc.name })
45
+ }
46
+ } else if (m.role === 'tool') {
47
+ // Backfill matching tool item with its result. The tool entry was
48
+ // pushed when we saw the assistant's tool_call above; find by id.
49
+ const idx = out.findIndex((it) => it.kind === 'tool' && it.id === m.toolCallId)
50
+ if (idx >= 0) {
51
+ const existing = out[idx]
52
+ if (existing && existing.kind === 'tool') {
53
+ out[idx] = { ...existing, result: m.content }
54
+ }
55
+ }
56
+ }
57
+ }
58
+ return out
59
+ }
60
+
61
+ /**
62
+ * Apply a SessionEvent emitted during a turn to the running items list.
63
+ * Pure — caller commits the new array via setState.
64
+ */
65
+ function applyEvent(items: Item[], ev: SessionEvent): Item[] {
66
+ switch (ev.type) {
67
+ case 'user_message':
68
+ // Already appended locally on send; ignore the echo.
69
+ return items
70
+ case 'assistant_message':
71
+ return [...items, { id: nextItemId(), kind: 'assistant', text: ev.text }]
72
+ case 'assistant_delta': {
73
+ const last = items[items.length - 1]
74
+ if (last && last.kind === 'assistant') {
75
+ return [...items.slice(0, -1), { ...last, text: last.text + ev.delta }]
76
+ }
77
+ return [...items, { id: nextItemId(), kind: 'assistant', text: ev.delta }]
78
+ }
79
+ case 'tool_call':
80
+ return [...items, { id: ev.id, kind: 'tool', name: ev.name }]
81
+ case 'tool_result':
82
+ case 'tool_error': {
83
+ const idx = items.findIndex((it) => it.kind === 'tool' && it.id === ev.id)
84
+ if (idx < 0) return items
85
+ const existing = items[idx]
86
+ if (!existing || existing.kind !== 'tool') return items
87
+ const updated: Item =
88
+ ev.type === 'tool_result'
89
+ ? { ...existing, result: ev.result }
90
+ : { ...existing, error: ev.error }
91
+ return [...items.slice(0, idx), updated, ...items.slice(idx + 1)]
92
+ }
93
+ case 'error':
94
+ return [...items, { id: nextItemId(), kind: 'error', text: ev.error }]
95
+ default:
96
+ return items
97
+ }
98
+ }
99
+
100
+ export default function ChatScreen() {
101
+ const { id } = useLocalSearchParams<{ id: string }>()
102
+ const [items, setItems] = useState<Item[]>([])
103
+ const [creds, setCreds] = useState<Credentials | null>(null)
104
+ const [draft, setDraft] = useState('')
105
+ const [loadState, setLoadState] = useState<'loading' | 'ready' | 'error'>('loading')
106
+ const [loadError, setLoadError] = useState<string | null>(null)
107
+ const [sending, setSending] = useState(false)
108
+ const colors = useColors()
109
+ const styles = useMemo(() => makeStyles(colors), [colors])
110
+ // Real header height (varies with notch / dynamic island / safe-area
111
+ // insets) so KeyboardAvoidingView lifts the input box above the keyboard
112
+ // by exactly the right amount.
113
+ const headerHeight = useHeaderHeight()
114
+ // Bottom inset clears the home indicator on iPhones with curved
115
+ // corners — without this, the input row gets clipped at rest.
116
+ const insets = useSafeAreaInsets()
117
+
118
+ // FlatList is `inverted` so the visual bottom is index 0 of the rendered
119
+ // data — chronological items get reversed at render time. This makes the
120
+ // list anchor itself to the bottom by default; no scrollToEnd dance.
121
+ const reversedItems = useMemo(() => [...items].reverse(), [items])
122
+
123
+ // History fetch on mount.
124
+ useEffect(() => {
125
+ let cancelled = false
126
+ ;(async () => {
127
+ const c = await loadCredentials()
128
+ if (!c) {
129
+ router.replace('/pair')
130
+ return
131
+ }
132
+ if (cancelled) return
133
+ setCreds(c)
134
+ try {
135
+ const res = await fetch(`${c.server}/api/agents/${id}/sessions/messages`, {
136
+ headers: { authorization: `Bearer ${c.token}`, origin: c.server },
137
+ })
138
+ if (res.status === 401) {
139
+ await clearCredentials()
140
+ router.replace('/pair')
141
+ return
142
+ }
143
+ if (!res.ok) {
144
+ throw new Error(`server returned ${res.status} loading history`)
145
+ }
146
+ const body = (await res.json()) as { messages: ProviderMessage[] }
147
+ if (cancelled) return
148
+ setItems(historyToItems(body.messages))
149
+ setLoadState('ready')
150
+ } catch (err) {
151
+ if (cancelled) return
152
+ setLoadError(err instanceof Error ? err.message : 'unknown error')
153
+ setLoadState('error')
154
+ }
155
+ })()
156
+ return () => {
157
+ cancelled = true
158
+ }
159
+ }, [id])
160
+
161
+ const onSend = useCallback(async () => {
162
+ if (!creds || sending) return
163
+ const text = draft.trim()
164
+ if (!text) return
165
+
166
+ setDraft('')
167
+ setSending(true)
168
+ setItems((prev) => [...prev, { id: nextItemId(), kind: 'user', text }])
169
+
170
+ try {
171
+ const res = await fetch(`${creds.server}/api/agents/${id}/chat`, {
172
+ method: 'POST',
173
+ headers: {
174
+ authorization: `Bearer ${creds.token}`,
175
+ origin: creds.server,
176
+ 'content-type': 'application/json',
177
+ },
178
+ body: JSON.stringify({ message: text }),
179
+ })
180
+ if (res.status === 401) {
181
+ await clearCredentials()
182
+ router.replace('/pair')
183
+ return
184
+ }
185
+ if (!res.ok) {
186
+ const errBody = (await res.json().catch(() => ({ error: res.statusText }))) as {
187
+ error?: string
188
+ }
189
+ throw new Error(errBody.error ?? `server returned ${res.status}`)
190
+ }
191
+ // Buffered NDJSON: read the whole body, split per line, parse each
192
+ // ChatFrame, then apply contained SessionEvents in order. RN's
193
+ // ReadableStream support is platform-flaky; buffering trades the
194
+ // typing-effect for reliability.
195
+ const raw = await res.text()
196
+ const frames: ChatFrame[] = raw
197
+ .split('\n')
198
+ .filter((l) => l.trim())
199
+ .map((l) => JSON.parse(l) as ChatFrame)
200
+
201
+ let next = items
202
+ for (const frame of frames) {
203
+ if (frame.kind === 'event') {
204
+ next = applyEvent(next, frame.event)
205
+ // Apply via setState too so each frame committed visibly when
206
+ // the chunk lands. (We defer to the final setState below for
207
+ // efficiency; this comment notes intent.)
208
+ } else if (frame.kind === 'fatal') {
209
+ next = [...next, { id: nextItemId(), kind: 'error', text: frame.error }]
210
+ }
211
+ }
212
+ setItems(next)
213
+ } catch (err) {
214
+ const message = err instanceof Error ? err.message : 'unknown error'
215
+ setItems((prev) => [...prev, { id: nextItemId(), kind: 'error', text: message }])
216
+ } finally {
217
+ setSending(false)
218
+ }
219
+ }, [creds, draft, id, items, sending])
220
+
221
+ if (loadState === 'loading') {
222
+ return (
223
+ <View style={styles.centered}>
224
+ <ActivityIndicator />
225
+ </View>
226
+ )
227
+ }
228
+
229
+ if (loadState === 'error') {
230
+ return (
231
+ <View style={styles.centered}>
232
+ <Text style={styles.errorTitle}>Couldn't load chat</Text>
233
+ <Text style={styles.errorBody}>{loadError}</Text>
234
+ </View>
235
+ )
236
+ }
237
+
238
+ return (
239
+ <KeyboardAvoidingView
240
+ style={styles.screen}
241
+ behavior={Platform.OS === 'ios' ? 'padding' : undefined}
242
+ keyboardVerticalOffset={headerHeight}
243
+ >
244
+ <FlatList
245
+ data={reversedItems}
246
+ inverted
247
+ keyExtractor={(it) => it.id}
248
+ renderItem={({ item }) => <Bubble item={item} />}
249
+ contentContainerStyle={styles.listContent}
250
+ ListEmptyComponent={
251
+ <View style={styles.empty}>
252
+ <Text style={styles.emptyText}>Start the conversation.</Text>
253
+ </View>
254
+ }
255
+ />
256
+ {sending ? (
257
+ <View style={styles.thinking}>
258
+ <ActivityIndicator size="small" color={colors.mochaLight} />
259
+ <Text style={styles.thinkingText}>thinking…</Text>
260
+ </View>
261
+ ) : null}
262
+ <View style={[styles.inputRow, { paddingBottom: 4 + insets.bottom }]}>
263
+ <TextInput
264
+ value={draft}
265
+ onChangeText={setDraft}
266
+ placeholder="Message"
267
+ placeholderTextColor={colors.fawn}
268
+ style={styles.input}
269
+ multiline
270
+ editable={!sending}
271
+ />
272
+ <Pressable
273
+ onPress={onSend}
274
+ disabled={sending || !draft.trim()}
275
+ style={({ pressed }) => [
276
+ styles.sendBtn,
277
+ (sending || !draft.trim()) && styles.sendBtnDisabled,
278
+ pressed && styles.sendBtnPressed,
279
+ ]}
280
+ >
281
+ <Text style={styles.sendBtnText}>Send</Text>
282
+ </Pressable>
283
+ </View>
284
+ </KeyboardAvoidingView>
285
+ )
286
+ }
287
+
288
+ function Bubble({ item }: { item: Item }) {
289
+ const colors = useColors()
290
+ const styles = useMemo(() => makeStyles(colors), [colors])
291
+ const markdownStyles = useMemo(() => makeMarkdownStyles(colors), [colors])
292
+ if (item.kind === 'user') {
293
+ return (
294
+ <View style={[styles.bubbleRow, styles.bubbleRowRight]}>
295
+ <View style={[styles.bubble, styles.bubbleUser]}>
296
+ <Text style={styles.bubbleUserText}>{item.text}</Text>
297
+ </View>
298
+ </View>
299
+ )
300
+ }
301
+ if (item.kind === 'assistant') {
302
+ return (
303
+ <View style={[styles.bubbleRow, styles.bubbleRowLeft]}>
304
+ <View style={[styles.bubble, styles.bubbleAssistant]}>
305
+ <Markdown style={markdownStyles}>{item.text}</Markdown>
306
+ </View>
307
+ </View>
308
+ )
309
+ }
310
+ if (item.kind === 'tool') {
311
+ const status = item.error ? 'error' : item.result !== undefined ? 'done' : 'running'
312
+ return (
313
+ <View style={styles.toolRow}>
314
+ <Text style={styles.toolLabel}>
315
+ ⚙ {item.name} · {status}
316
+ </Text>
317
+ {item.error ? (
318
+ <Text style={styles.toolError} numberOfLines={3}>
319
+ {item.error}
320
+ </Text>
321
+ ) : item.result ? (
322
+ <Text style={styles.toolResult} numberOfLines={4}>
323
+ {item.result}
324
+ </Text>
325
+ ) : null}
326
+ </View>
327
+ )
328
+ }
329
+ return (
330
+ <View style={styles.errorRow}>
331
+ <Text style={styles.errorBubbleText}>error: {item.text}</Text>
332
+ </View>
333
+ )
334
+ }
335
+
336
+ const makeStyles = (colors: Colors) =>
337
+ StyleSheet.create({
338
+ screen: { flex: 1, backgroundColor: colors.background },
339
+ centered: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 24, gap: 12 },
340
+ listContent: { padding: 12, paddingBottom: 24, gap: 8 },
341
+ empty: { padding: 32, alignItems: 'center' },
342
+ emptyText: { color: colors.mochaLight, fontSize: 13, fontFamily: fonts.body },
343
+ bubbleRow: { flexDirection: 'row' },
344
+ bubbleRowLeft: { justifyContent: 'flex-start' },
345
+ bubbleRowRight: { justifyContent: 'flex-end' },
346
+ bubble: { maxWidth: '85%', paddingHorizontal: 12, paddingVertical: 8, borderRadius: radii.lg },
347
+ bubbleUser: { backgroundColor: colors.sapphire },
348
+ bubbleUserText: { color: colors.primaryForeground, fontSize: 15, fontFamily: fonts.body },
349
+ bubbleAssistant: { backgroundColor: colors.card },
350
+ bubbleAssistantText: { color: colors.foreground, fontSize: 15, fontFamily: fonts.body },
351
+ toolRow: {
352
+ paddingHorizontal: 12,
353
+ paddingVertical: 6,
354
+ backgroundColor: colors.ivory,
355
+ borderRadius: radii.md,
356
+ borderWidth: StyleSheet.hairlineWidth,
357
+ borderColor: colors.border,
358
+ gap: 4,
359
+ },
360
+ toolLabel: { fontFamily: fonts.mono, fontSize: 12, color: colors.mocha },
361
+ toolResult: { fontFamily: fonts.mono, fontSize: 11, color: colors.mochaLight },
362
+ toolError: { fontFamily: fonts.mono, fontSize: 11, color: colors.destructive },
363
+ errorRow: {
364
+ paddingHorizontal: 12,
365
+ paddingVertical: 8,
366
+ // 6-char hex + 1A alpha suffix = ~10% opacity; tracks colors.destructive
367
+ // across both themes without needing a separate `destructiveBg` token.
368
+ backgroundColor: `${colors.destructive}1A`,
369
+ borderRadius: radii.md,
370
+ },
371
+ errorBubbleText: { color: colors.destructive, fontSize: 13, fontFamily: fonts.body },
372
+ thinking: {
373
+ flexDirection: 'row',
374
+ alignItems: 'center',
375
+ gap: 8,
376
+ paddingHorizontal: 16,
377
+ paddingVertical: 6,
378
+ },
379
+ thinkingText: {
380
+ color: colors.mochaLight,
381
+ fontSize: 12,
382
+ fontStyle: 'italic',
383
+ fontFamily: fonts.body,
384
+ },
385
+ inputRow: {
386
+ flexDirection: 'row',
387
+ alignItems: 'flex-end',
388
+ gap: 8,
389
+ paddingHorizontal: 12,
390
+ paddingTop: 8,
391
+ paddingBottom: 12,
392
+ borderTopWidth: StyleSheet.hairlineWidth,
393
+ borderTopColor: colors.border,
394
+ backgroundColor: colors.background,
395
+ },
396
+ input: {
397
+ flex: 1,
398
+ minHeight: 40,
399
+ maxHeight: 120,
400
+ paddingHorizontal: 12,
401
+ paddingVertical: 8,
402
+ borderRadius: radii.xl,
403
+ backgroundColor: colors.ivory,
404
+ fontSize: 15,
405
+ color: colors.foreground,
406
+ fontFamily: fonts.body,
407
+ },
408
+ sendBtn: {
409
+ paddingHorizontal: 16,
410
+ paddingVertical: 10,
411
+ borderRadius: radii.xl,
412
+ backgroundColor: colors.sapphire,
413
+ alignSelf: 'flex-end',
414
+ },
415
+ sendBtnDisabled: { backgroundColor: colors.mochaLight },
416
+ sendBtnPressed: { backgroundColor: colors.sapphireDeep },
417
+ sendBtnText: { color: colors.primaryForeground, fontSize: 14, fontFamily: fonts.bodyMedium },
418
+ errorTitle: { fontSize: 18, fontFamily: fonts.bodyBold, color: colors.foreground },
419
+ errorBody: { color: colors.destructive, textAlign: 'center', fontFamily: fonts.body },
420
+ })
421
+
422
+ // Style overrides for assistant-bubble markdown. Matches the surrounding
423
+ // bubble look (Baziu body font, foreground color) and trims default
424
+ // vertical paragraph margins so short replies don't get extra space.
425
+ const makeMarkdownStyles = (colors: Colors) =>
426
+ StyleSheet.create({
427
+ body: { color: colors.foreground, fontSize: 15, fontFamily: fonts.body },
428
+ paragraph: { marginTop: 0, marginBottom: 0 },
429
+ heading1: {
430
+ fontSize: 22,
431
+ fontFamily: fonts.display,
432
+ color: colors.foreground,
433
+ marginTop: 4,
434
+ marginBottom: 4,
435
+ },
436
+ heading2: {
437
+ fontSize: 19,
438
+ fontFamily: fonts.display,
439
+ color: colors.foreground,
440
+ marginTop: 4,
441
+ marginBottom: 4,
442
+ },
443
+ heading3: {
444
+ fontSize: 17,
445
+ fontFamily: fonts.bodyBold,
446
+ color: colors.foreground,
447
+ marginTop: 4,
448
+ marginBottom: 4,
449
+ },
450
+ strong: { fontFamily: fonts.bodyBold },
451
+ code_inline: {
452
+ fontFamily: fonts.mono,
453
+ fontSize: 13,
454
+ backgroundColor: colors.frost,
455
+ color: colors.charcoal,
456
+ paddingHorizontal: 4,
457
+ borderRadius: 4,
458
+ },
459
+ code_block: {
460
+ fontFamily: fonts.mono,
461
+ fontSize: 12,
462
+ backgroundColor: colors.frost,
463
+ color: colors.charcoal,
464
+ padding: 8,
465
+ borderRadius: radii.sm,
466
+ },
467
+ fence: {
468
+ fontFamily: fonts.mono,
469
+ fontSize: 12,
470
+ backgroundColor: colors.frost,
471
+ color: colors.charcoal,
472
+ padding: 8,
473
+ borderRadius: radii.sm,
474
+ },
475
+ link: { color: colors.sapphireDeep },
476
+ blockquote: {
477
+ backgroundColor: colors.ivory,
478
+ borderLeftColor: colors.sapphire,
479
+ borderLeftWidth: 3,
480
+ paddingLeft: 8,
481
+ paddingVertical: 4,
482
+ marginVertical: 4,
483
+ },
484
+ bullet_list: { marginVertical: 2 },
485
+ ordered_list: { marginVertical: 2 },
486
+ })