3An Android client for the [crush](https://github.com/charmbracelet/crush) AI coding
4assistant's server mode. Built with Kotlin and Jetpack Compose.
8Crush has a client/server architecture. Running `crush server --host 0.0.0.0:8080`
9starts a daemon that exposes a REST API (v1) with real-time streaming via
10Server-Sent Events (SSE). This app connects to that server over HTTP, lets you
11pick a workspace (a directory on the server), and chat with the crush agent —
12sending prompts, receiving streamed responses, and approving/denying tool
13permission requests — all from your phone.
16┌─────────────┐ HTTP + SSE ┌──────────────┐
17│ Android │ ──────────────────→ │ crush │
18│ (this app) │ ←────────────────── │ server │
19└─────────────┘ /v1/* REST + SSE └──────────────┘
24- **Connect screen** — enter the server host and workspace path; connection
25 settings are persisted via DataStore.
26- **Chat UI** — message bubbles for user/assistant/tool messages, with
27 reasoning, tool calls, and tool results rendered inline.
28- **Session management** — create new sessions and switch between existing ones.
29- **SSE event streaming** — real-time message updates, agent events, and
30 run-completion via the `/v1/workspaces/{id}/events` endpoint.
31- **Permission requests** — tool permission requests from the agent surface as
32 dialogs; allow or deny with one tap.
33- **Cancel runs** — stop a busy agent run mid-stream.
37This project uses Nix for its dev shell (see `flake.nix`). The SDK platform
38directory from Nix is named `android-37.0`, but AGP expects `android-37`, so a
39small local SDK overlay is needed:
45# Create the corrected SDK overlay (one-time)
46SDK_ROOT="$ANDROID_HOME"
47LOCAL_SDK=".android-sdk"
48mkdir -p "$LOCAL_SDK/platforms"
49for d in build-tools cmdline-tools platform-tools tools licenses cmake; do
50 ln -s "$SDK_ROOT/$d" "$LOCAL_SDK/$d"
52cp -rL "$SDK_ROOT/platforms/android-37.0" "$LOCAL_SDK/platforms/android-37"
53sed -i 's/AndroidVersion.ApiLevel=37\.0/AndroidVersion.ApiLevel=37/' \
54 "$LOCAL_SDK/platforms/android-37/source.properties"
55sed -i 's/<api-level>37\.0<\/api-level>/<api-level>37<\/api-level>/' \
56 "$LOCAL_SDK/platforms/android-37/package.xml"
58# Point Gradle at the overlay
59echo "sdk.dir=$LOCAL_SDK" > local.properties
62./gradlew assembleDebug
65The debug APK lands at `app/build/outputs/apk/debug/app-debug.apk`.
691. Start the crush server on a machine reachable from your phone:
71 crush server --host 0.0.0.0:8080
732. Install the APK on your Android device.
743. Open the app, enter `http://<server-ip>:8080` and the workspace path
75 (e.g. `/home/you/project`).
764. Tap **Connect**, then **+** to create a session, and start chatting.
81app/src/main/java/soy/iko/crush/
82├── CrushApp.kt # Application + ViewModel factory
83├── MainActivity.kt # Single-activity Compose host
84├── proto/ # Protocol model classes (match crush's Go structs)
87│ ├── Message.kt # Message, ContentPart, PartWrapper (discriminated union)
88│ ├── Events.kt # AgentEvent, RunComplete, PermissionRequest, etc.
89│ ├── Payload.kt # SSE payload envelope (PayloadType discriminator)
92│ ├── CrushJson.kt # Shared kotlinx.serialization Json instance
93│ ├── CrushApiClient.kt # OkHttp REST client for the v1 API
94│ └── EventStream.kt # SSE parsing → Flow<Payload>
96│ ├── CrushRepository.kt # State aggregator: API + SSE → StateFlows
97│ └── SettingsStore.kt # DataStore preferences (host, workspace)
99 ├── ChatViewModel.kt # ViewModel exposing ChatUiState
100 ├── MessageRenderer.kt # Flattens message parts → display text
101 ├── theme/CrushTheme.kt # Material 3 color scheme
103 ├── ConnectScreen.kt # Server connection form
104 └── ChatScreen.kt # Chat + session picker + permission dialogs
109The crush server uses a discriminated-union pattern for message parts. Each part
110is wrapped as `{"type": "text"|"tool_call"|"tool_result"|…, "data": {…}}`. The
111Kotlin `ContentPart` data class has all possible fields as nullable, and
112`PartWrapper` carries the type discriminator. SSE events are wrapped in a
113`Payload` envelope: `{"type": "message"|"session"|"permission_request"|…,
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list