Skip to content
Merged
Show file tree
Hide file tree
Changes from 62 commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
197fe92
chore(mobile): add UI element lint guardrails and the unified element…
iscekic Oct 2, 2026
6cf4662
refactor(mobile): unify the sheet, dialog, list, image and video surf…
iscekic Oct 2, 2026
df02dc7
test(mobile): name the image viewer in the markdown image mount test
iscekic Oct 2, 2026
00b92f8
style(mobile): format the oxlint config for the repo-wide format check
iscekic Oct 2, 2026
45974ed
feat(mobile): clear the keyboard natively on the four forked screens
iscekic Oct 2, 2026
34d629a
refactor(mobile): delete the hand-rolled keyboard padding layer
iscekic Oct 2, 2026
3a3836e
test(mobile): honor the keyboard-state selector in the shared stub
iscekic Oct 2, 2026
6293067
refactor(mobile): delete the composer reveal hook and close the sheet…
iscekic Oct 2, 2026
109319d
refactor(mobile): move the page sheet and the image viewer onto Sheet
iscekic Oct 2, 2026
5aed203
refactor(mobile): one confirm and one form dialog, with no RN Modal
iscekic Oct 2, 2026
3ed1ff9
fix(mobile): repair native sheet, input, and keyboard layouts
iscekic Oct 2, 2026
fd87eae
fix(mobile): keep slash command names on one visible line
iscekic Oct 2, 2026
4906071
Merge main into mobile UI unification
iscekic Oct 2, 2026
dbd216f
docs(mobile): describe the shared multiline input inset
iscekic Oct 2, 2026
888d46f
fix(mobile): apply multiline padding on Android native inputs
iscekic Oct 3, 2026
ad708c7
fix(mobile): keep the session filter footer visible
iscekic Oct 3, 2026
978d619
fix(mobile): refresh searched sessions after mutation
iscekic Oct 3, 2026
25be731
fix(mobile): clear Android status bar in session filters
iscekic Oct 3, 2026
97a253b
fix(mobile): repair native spacing and duplicate status
iscekic Oct 3, 2026
0c39bde
test(mobile): remove foreground wiring-only assertions
iscekic Oct 3, 2026
d0b99ca
chore(mobile): ban Liquid Glass libraries
iscekic Oct 6, 2026
6530362
feat(mobile): add Rozenite TanStack Query and network DevTools panels
iscekic Oct 6, 2026
c0cb6d1
spike(mobile): render markdown runs with react-native-enriched-markdown
iscekic Oct 6, 2026
bb6b266
spike(mobile): draw mermaid fences with react-native-webview
iscekic Oct 6, 2026
9ba9f84
feat(mobile): add Rozenite navigation, file system, overlay, Atlas an…
iscekic Oct 6, 2026
7b1ad8b
fix(mobile): use the Android monospace font for enriched code
iscekic Oct 6, 2026
ac1d3a5
chore(mobile): upgrade react-native-enriched-markdown to 1.1.0
iscekic Oct 6, 2026
ed6e25f
feat(mobile): gate markdown images and open them in the image viewer
iscekic Oct 6, 2026
6414293
refactor(mobile): delete the marked markdown chain
iscekic Oct 6, 2026
3ac16bd
docs(mobile): map markdown and diagram elements to their libraries
iscekic Oct 6, 2026
b652736
feat(mobile): give iOS selectable code fences native range selection …
iscekic Oct 6, 2026
f6e3735
fix(mobile): satisfy lint in the UITextView fence
iscekic Oct 6, 2026
0e6e55d
fix(mobile): drop continue in the UITextView fence
iscekic Oct 6, 2026
9daa4c6
chore(mobile): refresh the lockfile for react-native-uitextview
iscekic Oct 6, 2026
139fb20
docs(mobile): turn Rozenite on through .env.local instead of a start …
iscekic Oct 6, 2026
d45407d
fix(mobile): keep enriched quotes and table rows readable in dark mod…
iscekic Oct 6, 2026
cb479b3
fix(mobile): leave fence long-press to the message details sheet
iscekic Oct 6, 2026
0852ff6
fix(mobile): satisfy lint in the enriched markdown, image gate and me…
iscekic Oct 6, 2026
dd9b934
docs(mobile): describe the Kilo Chat keyboard lift as it is built
iscekic Oct 6, 2026
89da13d
Merge remote-tracking branch 'origin/main' into spike/mermaid-webview
iscekic Oct 6, 2026
4ff7b27
fix(mobile): let the lexer decide which image copies the gate rewrites
iscekic Oct 6, 2026
23385a0
fix(mobile): escape mermaid source in the page script and parse its m…
iscekic Oct 6, 2026
ab78107
fix(mobile): cap the image gate's lexer probes per message
iscekic Oct 6, 2026
f8d4e83
feat(mobile): add the Rozenite performance monitor panel
iscekic Oct 6, 2026
f863315
feat(mobile): refresh the glanceables in the background when a push i…
iscekic Oct 6, 2026
2d9f17e
Merge remote-tracking branch 'origin/main' into spike/mermaid-webview
iscekic Oct 6, 2026
a5e1817
docs(mobile): turn Rozenite on through .env.development.local
iscekic Oct 6, 2026
4c6474b
fix(mobile): keep a cold-start background refresh from cancelling itself
iscekic Oct 6, 2026
08c493e
fix(mobile): build expo-background-task from source on Android
iscekic Oct 6, 2026
8808328
chore(mobile): remove Rozenite and point agents at agent-device
iscekic Oct 6, 2026
4347790
Merge remote-tracking branch 'origin/main' into spike/mermaid-webview
iscekic Oct 6, 2026
b8f417f
chore(mobile): request a kilobot review of the changes after the last…
iscekic Oct 6, 2026
b98924b
chore(mobile): upgrade intl-durationformat, Sentry and speech recogni…
iscekic Oct 6, 2026
7e91778
chore(mobile): move nativewind to the v5 Expo 57 release candidate
iscekic Oct 6, 2026
2eea545
chore(mobile): move the mobile TypeScript package to 7.0.2
iscekic Oct 6, 2026
fa7ca25
chore(mobile): upgrade Reanimated to 4.7.1 and Worklets to 0.13.0
iscekic Oct 6, 2026
3951992
chore(mobile): upgrade react-native-appsflyer to 7.0.2-v2
iscekic Oct 6, 2026
5c89d7c
fix(mobile): type-check the react-native-appsflyer 7 source
iscekic Oct 6, 2026
1fa9549
refactor(mobile): move the AppsFlyer integration to the v7 API
iscekic Oct 6, 2026
541ec4d
revert(mobile): keep nativewind on 5.0.0-preview.4
iscekic Oct 6, 2026
19ba4d0
Merge remote-tracking branch 'origin/main' into spike/mermaid-webview
iscekic Oct 6, 2026
7b18ce0
chore(mobile): request a kilobot review of the dependency upgrades
iscekic Oct 6, 2026
60fefc2
fix(mobile): ship the Mermaid script with the app
iscekic Oct 6, 2026
97bb7e2
fix(mobile): load the Mermaid script by file instead of inlining it
iscekic Oct 6, 2026
4e4f73e
docs(mobile): tell agents how to make E2E fixtures
iscekic Oct 6, 2026
97e2818
feat(mobile): convert HTML that markdown can express into markdown
iscekic Oct 6, 2026
3daa870
feat(mobile): render remaining HTML with @native-html/render
iscekic Oct 6, 2026
a345ab7
Merge commit '4e4f73e1c3' into spike/markdown-html
iscekic Oct 6, 2026
3ba5390
chore(dev): add the mobile markdown fixture seed topic
iscekic Oct 6, 2026
f6bf853
fix(mobile): render unknown HTML tags as inline text
iscekic Oct 6, 2026
2f347f9
fix(mobile): put HTML subscripts and keys on the text baseline
iscekic Oct 6, 2026
a88c747
test(mobile): assert kbd and sub/sup by behavior, not class names
iscekic Oct 6, 2026
9d8c2e5
fix(mobile): type-check the HTML renderers with Expo's web types present
iscekic Oct 6, 2026
46d9c9b
fix(mobile): rewrite the Mermaid host page on each launch so page edi…
iscekic Oct 6, 2026
08b02de
Merge remote-tracking branch 'origin/main' into spike/mermaid-webview
iscekic Oct 6, 2026
ebe0973
fix(mobile): mask indented code after headings and fences, and close …
iscekic Oct 6, 2026
886f09d
fix(mobile): render HTML keys and scripts on the text baseline on iOS
iscekic Oct 6, 2026
8834d3b
fix(mobile): keep setext, sub/sup edge spaces and adjacent code spans…
iscekic Oct 6, 2026
56e4c78
Merge main into PR 7110 and resolve mobile conflicts
iscekic Oct 8, 2026
edf0397
test(mobile): isolate native dialogs in profile query harness
iscekic Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
147 changes: 145 additions & 2 deletions apps/mobile/.oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
{
"name": "no-literal-copy",
"specifier": "../../tools/oxlint/no-literal-copy/index.ts"
},
{
"name": "no-destructive-alert",
"specifier": "../../tools/oxlint/no-destructive-alert.mjs"
}
],
"options": {
Expand Down Expand Up @@ -54,8 +58,15 @@
"paths": [
{
"name": "react-native",
"importNames": ["Image"],
"message": "Import Image from @/components/ui/image instead."
"importNames": [
"Image",
"Modal",
"KeyboardAvoidingView",
"FlatList",
"VirtualizedList",
"SectionList"
],
"message": "Image → @/components/ui/image. Modal → @/components/ui/sheet or a formSheet route. KeyboardAvoidingView → react-native-keyboard-controller. FlatList/VirtualizedList/SectionList → FlashList. See apps/mobile/AGENTS.md."
},
{
"name": "expo-image",
Expand All @@ -68,6 +79,118 @@
{
"name": "expo-file-system/legacy",
"message": "Use the modern expo-file-system API (File, Directory, Paths, UploadType). The legacy module is deprecated."
},
{
"name": "@legendapp/list",
"message": "FlashList is the app's list component."
},
{
"name": "@expo/ui/community/bottom-sheet",
"message": "Use Sheet from @/components/ui/sheet."
},
{
"name": "@gorhom/bottom-sheet",
"message": "Use Sheet from @/components/ui/sheet."
},
{
"name": "react-native-modal",
"message": "Use Sheet from @/components/ui/sheet."
},
{
"name": "react-native-modalize",
"message": "Use Sheet from @/components/ui/sheet."
},
{
"name": "@lodev09/react-native-true-sheet",
"message": "Use Sheet from @/components/ui/sheet."
},
{
"name": "@swmansion/react-native-bottom-sheet",
"message": "Use Sheet from @/components/ui/sheet."
},
{
"name": "@rn-primitives/dialog",
"message": "Use the dialog surfaces from @/components/ui/dialog."
},
{
"name": "react-native-keyboard-aware-scroll-view",
"message": "Use KeyboardAwareScrollView from react-native-keyboard-controller."
},
{
"name": "react-native-toast-message",
"message": "Use sonner-native."
},
{
"name": "burnt",
"message": "Use sonner-native."
},
{
"name": "moti",
"message": "Use @/components/ui/skeleton."
},
{
"name": "react-native-reanimated-skeleton",
"message": "Use @/components/ui/skeleton."
},
{
"name": "react-native-skeleton-placeholder",
"message": "Use @/components/ui/skeleton."
},
{
"name": "react-hook-form",
"message": "Use @/components/ui/form-field."
},
{
"name": "@tanstack/react-form",
"message": "Use @/components/ui/form-field."
},
{
"name": "react-native-awesome-gallery",
"message": "Use @/components/ui/image-viewer."
},
{
"name": "react-native-image-viewing",
"message": "Use @/components/ui/image-viewer."
},
{
"name": "react-native-image-zoom-viewer",
"message": "Use @/components/ui/image-viewer."
},
{
"name": "@likashefqet/react-native-image-zoom",
"message": "Use @/components/ui/image-viewer."
},
{
"name": "react-native-video",
"message": "Use expo-video."
},
{
"name": "expo-av",
"message": "Use expo-video and expo-audio."
},
{
"name": "expo-glass-effect",
"message": "The app does not use Liquid Glass. Use expo-blur."
},
{
"name": "@callstack/liquid-glass",
"message": "The app does not use Liquid Glass. Use expo-blur."
},
{
"name": "@react-native-segmented-control/segmented-control",
"message": "Use @/components/ui/segmented-control."
},
{
"name": "react-native-markdown-display",
"message": "Use MarkdownText from @/components/agents/markdown-text."
},
{
"name": "@ronradtke/react-native-markdown-display",
"message": "Use MarkdownText from @/components/agents/markdown-text."
},
{
"name": "react-native-marked",
"message": "Use MarkdownText from @/components/agents/markdown-text."
}
]
}
Expand Down Expand Up @@ -220,6 +343,7 @@
"anti-slop/no-unknown-returns": "error",
"zod-utils/no-inline-zod-schema": "error",
"rn-modal-backdrop/require-backdrop": "error",
"no-destructive-alert/no-destructive-alert": "error",
"no-literal-copy/no-literal-copy": "error"
},
"overrides": [
Expand Down Expand Up @@ -254,6 +378,25 @@
"no-restricted-imports": "off"
}
},
{
// Sanctioned homes for a banned import. These are the only files that may
// import a unified element's underlying library directly.
// sheet.tsx — the single home of `@expo/ui` BottomSheet
// dialog.tsx — the single home of `@rn-primitives/dialog`
// Test files mock `react-native` and the native libraries directly, so
// they are exempt permanently. No file imports `Modal` from
// `react-native` any more, and no exception exists for it: see
// apps/mobile/AGENTS.md.
"files": [
"src/components/ui/sheet.tsx",
"src/components/ui/dialog.tsx",
"**/*.test.ts",
"**/*.test.tsx"
],
"rules": {
"no-restricted-imports": "off"
}
},
{
"files": ["src/components/ui/icons.ts"],
"rules": {
Expand Down
85 changes: 84 additions & 1 deletion apps/mobile/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ git diff --check
- Fix lint rules in spirit: autofix first, then extract code. Never compress code to dodge line limits.
- Do not commit plans, specs, or other non-code Markdown files.
- The repository dev runner owns Metro. Never run `pnpm start`.
- Use `agent-device` (Callstack) to drive and inspect the app on a simulator, emulator, or device. Run `agent-device help workflow` first.
- Open the app with `agent-device open <app-id>`. Read the screen with `agent-device snapshot -i`, then act on its `@eN` refs with `press` and `fill`.
- Use `agent-device logs`, `network`, `perf`, `react-devtools`, and `trace` for app logs, HTTP traffic, frames and memory, render profiles, and traces.
- Use `agent-device metro` to reload the app. Do not start Metro yourself.
- Bind one session to one device with `--session <name>`. A session that is bound to another device rejects the command.

## Dependencies

Expand All @@ -42,6 +47,78 @@ git diff --check

Then restart Metro and force-quit the app.

## Library First

Before you build a UI element, find the library that already does it.

1. Check the element map below. If the concern has an entry, use that element.
2. Check `@expo/ui` (`node_modules/@expo/ui/build/`), which ships native modules for `57.x`: universal
`BottomSheet`, `Picker`, `Switch`, `TextInput`, `List`, `Host`, plus
`community/{segmented-control, picker, datetime-picker, masked-view, menu, pager-view, slider}`.
3. Check the Expo SDK 57 docs for the concern.
4. Check npm for a maintained package. A release in the last six months is the bar.

A hand-built element is correct only when steps 1–4 fail. Record the decision in the PR body: the library
and version you chose, or which candidates you rejected and why. "It was easier to write it" is not a
reason.

## Unified Elements

One element per concern. `no-restricted-imports` in `.oxlintrc.json` enforces the "Use instead" column.

| Concern | Element | Use instead of |
|---|---|---|
| Bottom sheet, imperative / non-route | `Sheet` from `@/components/ui/sheet` | `Modal` from `react-native`, `@gorhom/bottom-sheet`, `react-native-modal`, `react-native-modalize` |
| Bottom sheet, route | expo-router `formSheet` via `useFormSheetScreenOptions()` | a JS bottom-sheet library |
| Confirm that needs the red affordance | `useConfirmDialog()` from `@/components/ui/dialog` (a native sheet) | `Alert.alert` for a destructive confirm; a direct `@rn-primitives/dialog` import |
| Dialog form (a field, a form) | `DialogCard` from `@/components/ui/dialog` | a direct `@rn-primitives/dialog` import |
| System confirm, non-destructive | `Alert.alert` | — |
| Keyboard avoidance | `react-native-keyboard-controller` | `KeyboardAvoidingView` from `react-native` |
| Lists | `@shopify/flash-list` | `FlatList`, `VirtualizedList`, `SectionList`, `@legendapp/list` |
| Image viewer | `@/components/ui/image-viewer` | `react-native-image-viewing`, `react-native-awesome-gallery` |
| Video | `expo-video` | `react-native-video`, `expo-av` |
| Blur | `expo-blur` | `expo-glass-effect`, `@callstack/liquid-glass` (no Liquid Glass) |
| Toast | `sonner-native` | `react-native-toast-message`, `burnt` |
| Images | `@/components/ui/image` | `Image` from `react-native`, `expo-image` |
| Icons | `@/components/ui/icons` | `lucide-react-native` |
| Markdown | `MarkdownText` from `@/components/agents/markdown-text`, rendered by `react-native-enriched-markdown` | `react-native-marked`, `react-native-markdown-display` |
| Diagram | ` ```mermaid ` fences → `MarkdownMermaid` (`react-native-webview`) | — |

No file imports `Modal` from `react-native`, `@expo/ui/community/bottom-sheet` or `@rn-primitives/dialog`
outside `@/components/ui/sheet` and `@/components/ui/dialog`. A sheet or a confirm must be able to stack
above another native sheet, and a dialog rendered through `@rn-primitives/portal` lives in the app's React
tree, so it cannot: `useConfirmDialog` therefore presents a native sheet, not a portal card. A `DialogCard`
is a portal card and stays behind a presented sheet — never open one from sheet content. When a surface
needs to stack and a form must host it, make it a `formSheet` route or a `Sheet`.

Markdown images reach `react-native-enriched-markdown` only from HTTPS hosts the reader trusted or confirmed;
`markdown-image-gate` rewrites every other image into a "Load" link before the native view can fetch it.
`ImageViewer` measures its viewport and gives the zoom child concrete dimensions.
Do not use percentage dimensions inside `ResumableZoom`'s unconstrained child container.
Use explicit pixel sizes for minimum touch targets; native rem is 14 points.
Use `TabScreenScrollView` for scrolling screens under the absolute tab bar.
Use a concrete height and `flex: 0` for an inline `FlashList`; `maxHeight` alone does not create a viewport.
Omit empty-state descriptions that repeat the title or tell the user to use a disabled action.

Keyboard avoidance is `react-native-keyboard-controller`, wrapped in one `KeyboardProvider` at the app root.
`KeyboardAvoidingView` clears the IME on the session, history, quick-chat, session-detail, new-session,
conversation, manual-review and PR-discussion surfaces; `useKeyboardState` is the app's one keyboard-height read.
The conversation list follows the newest message when its viewport shrinks; it must not add a second keyboard inset.
A surface whose content
pads the platform's bottom inset itself passes `keyboardVerticalOffset={keyboardInsetOffset(bottom)}`
(`@/lib/keyboard-inset-offset`) — the provider's Android height spans the translucent navigation bar, so
without the reduction the content floats a navigation-bar height above the keyboard.

Nothing else may read the keyboard: no surface adds a listener beside the provider.

Android searchable `formSheet` routes use only the existing safe-area-capped full detent.
Model, repository, app-language, auth-language and voice-language searches must keep results above the software keyboard.
iOS keeps native detent expansion when the search field gains focus.

Scope each PR form header with a native safe-area provider outside the scroll view.
Apply its top inset with `SafeAreaView`, not the app root inset.
Direct full-screen entry clears the status bar; presented sheets do not add a second inset.

## Implementation Rules

- Write the smallest boring implementation. Reuse existing helpers, components, and contracts.
Expand All @@ -50,6 +127,7 @@ git diff --check
- Parse backend dates with `parseTimestamp()` from `@/lib/utils`; `new Date()` breaks on PostgreSQL timestamps in Hermes.
- Every mutation hook shows `toast.error(error.message)` in `onError`. Put shared error handling in the hook, not in each component.
- Use optimistic updates for obvious reversible mutations: snapshot in `onMutate`, roll back in `onError`, reconcile in `onSettled`.
- After a session mutation, refresh stored search queries as well as the list through `invalidateAgentSessionQueries`.
- Keep route files thin. Put screen logic in components or hooks.

## React Native Rules
Expand All @@ -69,12 +147,17 @@ git diff --check
- Use `defaultValue` only for initial content.
- Single-line inputs: use `leading-[normal]`. A `lineHeight` above the font's natural one (which `text-sm`/`text-base` set on their own) makes iOS draw the placeholder lower than the typed text and clip it. Multi-line inputs keep an explicit `leading-*`.
- Single-line inputs: set the height with `min-h-*`, not `py-*`. iOS insets the already-centered text rect by the padding, so vertical padding draws the text and the placeholder low.
- Use the shared `Input` for every single-line field.
- `Input` removes vertical padding, centers Android text, and defaults iOS line breaks to `clip`.
- A single-line caller can change horizontal padding, text size, and minimum height.
- All inputs use physical `pl-*`/`pr-*` for horizontal padding, including caller overrides and zero-inset search fields; Android `TextInput` does not apply logical `paddingInline` from `px-*`.
- A multiline caller keeps its alignment and line breaks; its physical padding classes override the shared `pl-3 pr-3 pt-2.5 pb-2.5` inset.
- Put input screens in a `ScrollView` with `automaticallyAdjustKeyboardInsets`.

## UI and UX Rules

- `ScreenHeader` is the first child of the screen root; set stack `headerShown: false`.
- Prefer native sheets, alerts, pickers, gestures, and keyboard behavior. Confirm destructive actions with `Alert.alert()`.
- Prefer native sheets, alerts, pickers, gestures, and keyboard behavior. Confirm a non-destructive action with `Alert.alert()`. Confirm a destructive action with `@/components/ui/dialog`, because Android's native `AlertDialog` drops the `style: 'destructive'` affordance.
- Every pressable gives lightweight feedback unless navigation or a native control already provides it.
- Every data screen handles loading, empty, error, and happy states. Use `Skeleton` matching final dimensions, `EmptyState`, and pagination when results can grow.
- Use `ActivityIndicator` only for inline waits. Where layout would jump, use the existing Reanimated `FadeIn`/`FadeOut`/`LinearTransition` patterns.
Expand Down
6 changes: 6 additions & 0 deletions apps/mobile/app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,8 @@ const config: ExpoConfig = {
],
'expo-router',
'expo-image',
// Inline video file parts in the session transcript (file-part-renderer).
'expo-video',
'expo-font',
// The app owns its Android backup rules (plugins/withAndroidManifestFix.js
// writes the union of the SecureStore and AppsFlyer exclusions). Disable the
Expand Down Expand Up @@ -350,6 +352,10 @@ const config: ExpoConfig = {
enableBackgroundRemoteNotifications: true,
},
],
// Registers the BGTaskScheduler identifier and the `processing` background
// mode for the periodic glanceable refresh, the fallback for a dropped push
// (src/lib/glanceable-refresh-task.ts).
'expo-background-task',
'expo-web-browser',
// Must stay before '@sentry/react-native/expo': it edits the RNSentrySDK.start() line that plugin writes.
'./plugins/withReactMountLogBreadcrumbs',
Expand Down
9 changes: 9 additions & 0 deletions apps/mobile/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,12 @@ require('./src/lib/notification-background-task')
.catch(() => {
// Registration already reports its own failure; the entry must never crash.
});

// The periodic glanceable refresh is the fallback for a data-only push the OS
// dropped. WorkManager (Android) and BGTaskScheduler (iOS) start it without the
// root layout, so it is registered here too (glanceable-refresh-task.ts).
require('./src/lib/glanceable-refresh-task')
.registerGlanceableRefreshTask()
.catch(() => {
// Registration already reports its own failure; the entry must never crash.
});
Loading
Loading