git.iko.soy/opencode-android
on master help sign in
Remove entire opencode-android project
commit2678e0d7881de526dbaa31f10884e777da1f103f authoriko <mail@iko.soy> dateWed 8 Jul 2026, 07:10 (3 months ago) parent52cef25
55 files changed−6388
README.md−104 app/build.gradle.kts−92 app/proguard-rules.pro−8 app/src/main/AndroidManifest.xml−27 app/src/main/java/soy/iko/opencode/CrashLogger.kt−74 app/src/main/java/soy/iko/opencode/MainActivity.kt−39 app/src/main/java/soy/iko/opencode/OpencodeApp.kt−10 app/src/main/java/soy/iko/opencode/core/Core.kt−98 app/src/main/java/soy/iko/opencode/core/CoreFfi.kt−44 app/src/main/java/soy/iko/opencode/core/HttpClient.kt−74 app/src/main/java/soy/iko/opencode/core/PureCoreFfi.kt−347 app/src/main/java/soy/iko/opencode/core/SseClient.kt−67 app/src/main/java/soy/iko/opencode/ui/screens/ChatScreen.kt−212 app/src/main/java/soy/iko/opencode/ui/screens/ConnectScreen.kt−109 app/src/main/java/soy/iko/opencode/ui/screens/SessionsScreen.kt−101 app/src/main/java/soy/iko/opencode/ui/theme/Theme.kt−58 app/src/main/res/values-night/themes.xml−4 app/src/main/res/values/strings.xml−3 app/src/main/res/values/themes.xml−4 app/src/test/java/soy/iko/opencode/core/PureCoreFfiTest.kt−136 build.gradle.kts−7 flake.lock−61 flake.nix−71 generated/build.gradle.kts−21 generated/com/novi/bincode/BincodeDeserializer.kt−39 generated/com/novi/bincode/BincodeSerializer.kt−33 generated/com/novi/serde/BinaryDeserializer.kt−182 generated/com/novi/serde/BinarySerializer.kt−122 generated/com/novi/serde/Bytes.kt−39 generated/com/novi/serde/DeserializationError.kt−6 generated/com/novi/serde/Deserializer.kt−76 generated/com/novi/serde/Int128.kt−6 generated/com/novi/serde/SerdeByteArrayOutput.kt−48 generated/com/novi/serde/SerializationError.kt−6 generated/com/novi/serde/Serializer.kt−77 generated/com/novi/serde/Slice.kt−36 generated/com/novi/serde/Tuple4.kt−6 generated/com/novi/serde/Tuple5.kt−12 generated/com/novi/serde/Tuple6.kt−13 generated/com/novi/serde/UInt128.kt−6 generated/soy/iko/opencode/core/Core.kt−1110 gradle.properties−3 gradle/libs.versions.toml−50 gradle/wrapper/gradle-wrapper.jar gradle/wrapper/gradle-wrapper.properties−7 gradlew−251 gradlew.bat−94 settings.gradle.kts−25 shared/Cargo.lock−1707 shared/Cargo.toml−31 shared/build.gradle.kts−39 shared/src/app.rs−494 shared/src/bin/codegen.rs−40 shared/src/ffi.rs−53 shared/src/lib.rs−6
README.mddeleted−104
@@ -1,104 +0,0 @@
1−# opencode-android
2−
3−An Android client for the [opencode](https://github.com/sst/opencode) server, built with
4−[Crux](https://github.com/redbadger/crux) (shared Rust core + Kotlin shell) and Jetpack Compose.
5−
6−## Architecture
7−
8−```
9−┌─────────────────────────────────────────────────┐
10−│ Shell (Kotlin / Jetpack Compose) │
11−│ ┌─────────────┐ ┌──────────────────────────┐ │
12−│ │ Compose UI │ │ HttpClient / SseClient │ │
13−│ │ (3 screens) │ │ (Ktor + OkHttp) │ │
14−│ └──────┬───────┘ └────────────┬─────────────┘ │
15−│ │ │ │
16−│ Event (bincode) Effect (bincode) │
17−│ │ ▲ │
18−│ ┌──────▼───────────────────────┴────────────┐ │
19−│ │ Core (ViewModel) — FFI bridge │ │
20−│ │ CoreFfi → NativeCoreFfi / PureCoreFfi │ │
21−│ └───────────────────────────────────────────┘ │
22−└──────────┬──────────────────────────────────────┘
23− │ JNI (bincode bytes)
24−┌──────────▼──────────────────────────────────────┐
25−│ Core (Rust / Crux) │
26−│ ┌────────┐ ┌─────────┐ ┌──────────┐ │
27−│ │ App │ │ Model │ │ Commands │ │
28−│ │ update │ │ state │ │ effects │ │
29−│ └────────┘ └─────────┘ └──────────┘ │
30−│ No side-effects — pure logic │
31−└─────────────────────────────────────────────────┘
32−```
33−
34−### Rust Core (`shared/`)
35−- **`app.rs`** — Crux `App` implementation: `Event`, `Model`, `ViewModel`, `Effect`, `update()`, `view()`
36−- **`ffi.rs`** — BoltFFI export wrapping `crux_core::bridge::Bridge`
37−- **`bin/codegen.rs`** — Type generator (produces Kotlin types via `facet-generate`)
38−
39−### Kotlin Shell (`app/`)
40−- **`core/CoreFfi.kt`** — FFI interface + JNI bridge (loads `libopencode.so`)
41−- **`core/PureCoreFfi.kt`** — Pure-Kotlin fallback (mirrors Rust logic for dev without NDK)
42−- **`core/Core.kt`** — Android ViewModel bridge: sends events, processes effects, exposes `StateFlow<ViewModel>`
43−- **`core/HttpClient.kt`** — Executes `Effect.Http` via Ktor/OkHttp, returns `HttpResult` to core
44−- **`core/SseClient.kt`** — Subscribes to opencode SSE event stream
45−- **`ui/screens/`** — Connect, Sessions, Chat screens (Jetpack Compose + Material 3)
46−
47−### Generated Types (`generated/`)
48−- Auto-generated Kotlin types matching the Rust core (Event, Effect, ViewModel, Request, etc.)
49−- Bincode serialization helpers (`com.novi.bincode`, `com.novi.serde`)
50−
51−## Building
52−
53−### Prerequisites
54−- Rust 1.90+ (with Android NDK targets for native builds)
55−- Android SDK (compile SDK 37, build-tools 37.0.0)
56−- JDK 17+
57−
58−### Generate Kotlin types
59−```sh
60−cd shared
61−cargo run --bin codegen --features codegen -- --language kotlin --output-dir ../generated
62−```
63−
64−### Build native library (requires NDK)
65−```sh
66−# Install Android targets
67−rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
68−
69−# Build and pack with BoltFFI
70−boltffi pack android
71−```
72−
73−### Build the APK
74−```sh
75−./gradlew assembleDebug
76−```
77−
78−The app works without the native library — `PureCoreFfi` provides a pure-Kotlin
79−reimplementation of the core logic. When `libopencode.so` is present, `NativeCoreFfi`
80−is used automatically.
81−
82−### Run tests
83−```sh
84−# Rust core tests
85−cd shared && cargo test
86−
87−# Kotlin shell tests
88−./gradlew testDebugUnitTest
89−```
90−
91−## API Coverage
92−
93−The client connects to an opencode server and supports:
94−- **Health check** (`GET /global/health`)
95−- **Session list** (`GET /session`)
96−- **Create session** (`POST /session`)
97−- **Load messages** (`GET /session/{id}/message`)
98−- **Send message** (`POST /session/{id}/prompt_async`)
99−- **SSE events** (`GET /event`) — live message streaming
100−
101−## Configuration
102−
103−The default server URL is `http://localhost:4096`. Change it in the Connect screen.
104−`android:usesCleartextTraffic="true"` is enabled for local development.
app/build.gradle.ktsdeleted−92
@@ -1,92 +0,0 @@
1−import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2−
3−plugins {
4− alias(libs.plugins.android.application)
5− alias(libs.plugins.kotlin.android)
6− alias(libs.plugins.kotlin.compose)
7− alias(libs.plugins.kotlin.serialization)
8−}
9−
10−android {
11− namespace = "soy.iko.opencode"
12− compileSdk = 37
13− buildToolsVersion = "37.0.0"
14−
15− defaultConfig {
16− applicationId = "soy.iko.opencode"
17− minSdk = 26
18− targetSdk = 37
19− versionCode = 1
20− versionName = "0.1.0"
21− testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
22− }
23−
24− signingConfigs {
25− create("release") {
26− storeFile = System.getenv("OPENCODE_STORE_FILE")?.let { file(it) }
27− storePassword = System.getenv("OPENCODE_STORE_PASSWORD")
28− keyAlias = System.getenv("OPENCODE_KEY_ALIAS")
29− keyPassword = System.getenv("OPENCODE_KEY_PASSWORD")
30− }
31− }
32−
33− buildTypes {
34− release {
35− isMinifyEnabled = false
36− signingConfig = signingConfigs.getByName("release")
37− proguardFiles(
38− getDefaultProguardFile("proguard-android-optimize.txt"),
39− "proguard-rules.pro",
40− )
41− }
42− }
43−
44− compileOptions {
45− sourceCompatibility = JavaVersion.VERSION_11
46− targetCompatibility = JavaVersion.VERSION_11
47− }
48−
49− kotlin {
50− compilerOptions {
51− jvmTarget = JvmTarget.JVM_11
52− }
53− }
54−
55− buildFeatures {
56− compose = true
57− }
58−}
59−
60−dependencies {
61− implementation(project(":shared"))
62−
63− implementation(libs.androidx.core.ktx)
64− implementation(libs.androidx.lifecycle.runtime.ktx)
65− implementation(libs.androidx.activity.compose)
66− implementation(platform(libs.androidx.compose.bom))
67− implementation(libs.androidx.compose.ui)
68− implementation(libs.androidx.compose.ui.graphics)
69− implementation(libs.androidx.compose.ui.tooling.preview)
70− implementation(libs.androidx.compose.material3)
71− implementation(libs.androidx.compose.material.icons.extended)
72− implementation(libs.lifecycle.viewmodel.compose)
73− implementation(libs.androidx.navigation.compose)
74−
75− implementation(libs.kotlinx.coroutines.android)
76−
77− implementation(libs.ktor.client.core)
78− implementation(libs.ktor.client.okhttp)
79− implementation(libs.ktor.client.content.negotiation)
80− implementation(libs.ktor.serialization.kotlinx.json)
81− implementation(libs.ktor.client.logging)
82− implementation(libs.ktor.client.auth)
83−
84− testImplementation(libs.junit)
85− testImplementation(libs.kotlinx.coroutines.test)
86− androidTestImplementation(libs.androidx.junit)
87− androidTestImplementation(libs.androidx.espresso.core)
88− androidTestImplementation(platform(libs.androidx.compose.bom))
89− androidTestImplementation(libs.androidx.compose.ui.test.junit4)
90− debugImplementation(libs.androidx.compose.ui.tooling)
91− debugImplementation(libs.androidx.compose.ui.test.manifest)
92−}
app/proguard-rules.prodeleted−8
@@ -1,8 +0,0 @@
1−# Keep generated Crux types (serialization reflection)
2−-keep class soy.iko.opencode.core.** { *; }
3−-keep class com.novi.serde.** { *; }
4−-keep class com.novi.bincode.** { *; }
5−
6−# Ktor
7−-keep class io.ktor.** { *; }
8−-dontwarn io.ktor.**
app/src/main/AndroidManifest.xmldeleted−27
@@ -1,27 +0,0 @@
1−<?xml version="1.0" encoding="utf-8"?>
2−<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3− xmlns:tools="http://schemas.android.com/tools">
4−
5− <uses-permission android:name="android.permission.INTERNET" />
6− <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
7−
8− <application
9− android:name=".OpencodeApp"
10− android:allowBackup="true"
11− android:label="@string/app_name"
12− android:supportsRtl="true"
13− android:theme="@style/Theme.Opencode"
14− android:usesCleartextTraffic="true"
15− tools:targetApi="31">
16− <activity
17− android:name=".MainActivity"
18− android:exported="true"
19− android:theme="@style/Theme.Opencode">
20− <intent-filter>
21− <action android:name="android.intent.action.MAIN" />
22− <category android:name="android.intent.category.LAUNCHER" />
23− </intent-filter>
24− </activity>
25− </application>
26−
27−</manifest>
app/src/main/java/soy/iko/opencode/CrashLogger.ktdeleted−74
@@ -1,74 +0,0 @@
1−package soy.iko.opencode
2−
3−import android.content.Context
4−import android.util.Log
5−import java.io.File
6−import java.io.PrintWriter
7−import java.io.StringWriter
8−import java.text.SimpleDateFormat
9−import java.util.Date
10−import java.util.Locale
11−
12−/**
13− * Lightweight crash reporter that installs as the global uncaught-exception
14− * handler. Crashes are logged to logcat and persisted to internal storage so
15− * they survive process death and can be inspected or exported later.
16− */
17−object CrashLogger {
18−
19− private const val TAG = "opencode-crash"
20− private const val CRASH_DIR = "crashes"
21− private const val MAX_REPORTS = 10
22−
23− private lateinit var appContext: Context
24− private var previousHandler: Thread.UncaughtExceptionHandler? = null
25−
26− fun init(context: Context) {
27− appContext = context.applicationContext
28− previousHandler = Thread.getDefaultUncaughtExceptionHandler()
29− Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
30− report(throwable, thread.name)
31− previousHandler?.uncaughtException(thread, throwable)
32− }
33− }
34−
35− fun report(throwable: Throwable, source: String = Thread.currentThread().name) {
36− val stackTrace = stackTraceString(throwable)
37− Log.e(TAG, "Uncaught exception in [$source]\n$stackTrace", throwable)
38− persistReport(source, stackTrace)
39− }
40−
41− fun getReports(): List<File> =
42− crashDir().listFiles()?.sortedByDescending { it.lastModified() } ?: emptyList()
43−
44− fun clearReports() {
45− crashDir().listFiles()?.forEach { it.delete() }
46− }
47−
48− private fun persistReport(source: String, stackTrace: String) {
49− try {
50− val timestamp = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.US).format(Date())
51− val file = File(crashDir(), "crash_${timestamp}.txt")
52− file.writeText("Time: $timestamp\nThread: $source\n\n$stackTrace")
53− pruneOldReports()
54− } catch (e: Exception) {
55− Log.e(TAG, "Failed to persist crash report", e)
56− }
57− }
58−
59− private fun pruneOldReports() {
60− val reports = getReports()
61− if (reports.size > MAX_REPORTS) {
62− reports.drop(MAX_REPORTS).forEach { it.delete() }
63− }
64− }
65−
66− private fun crashDir(): File =
67− File(appContext.filesDir, CRASH_DIR).apply { mkdirs() }
68−
69− private fun stackTraceString(throwable: Throwable): String {
70− val sw = StringWriter()
71− throwable.printStackTrace(PrintWriter(sw))
72− return sw.toString()
73− }
74−}
app/src/main/java/soy/iko/opencode/MainActivity.ktdeleted−39
@@ -1,39 +0,0 @@
1−package soy.iko.opencode
2−
3−import android.os.Bundle
4−import androidx.activity.ComponentActivity
5−import androidx.activity.compose.setContent
6−import androidx.activity.enableEdgeToEdge
7−import androidx.compose.runtime.Composable
8−import androidx.compose.runtime.collectAsState
9−import androidx.compose.runtime.getValue
10−import androidx.lifecycle.viewmodel.compose.viewModel
11−import soy.iko.opencode.core.Core
12−import soy.iko.opencode.core.Screen
13−import soy.iko.opencode.ui.screens.ChatScreen
14−import soy.iko.opencode.ui.screens.ConnectScreen
15−import soy.iko.opencode.ui.screens.SessionsScreen
16−import soy.iko.opencode.ui.theme.OpencodeTheme
17−
18−class MainActivity : ComponentActivity() {
19− override fun onCreate(savedInstanceState: Bundle?) {
20− super.onCreate(savedInstanceState)
21− enableEdgeToEdge()
22− setContent {
23− OpencodeTheme {
24− App()
25− }
26− }
27− }
28−}
29−
30−@Composable
31−fun App(core: Core = viewModel()) {
32− val view by core.view.collectAsState()
33−
34− when (view.screen) {
35− Screen.CONNECT -> ConnectScreen(core)
36− Screen.SESSIONS -> SessionsScreen(core)
37− Screen.CHAT -> ChatScreen(core)
38− }
39−}
app/src/main/java/soy/iko/opencode/OpencodeApp.ktdeleted−10
@@ -1,10 +0,0 @@
1−package soy.iko.opencode
2−
3−import android.app.Application
4−
5−class OpencodeApp : Application() {
6− override fun onCreate() {
7− super.onCreate()
8− CrashLogger.init(this)
9− }
10−}
app/src/main/java/soy/iko/opencode/core/Core.ktdeleted−98
@@ -1,98 +0,0 @@
1−package soy.iko.opencode.core
2−
3−import androidx.lifecycle.viewModelScope
4−import kotlinx.coroutines.flow.MutableStateFlow
5−import kotlinx.coroutines.flow.StateFlow
6−import kotlinx.coroutines.flow.asStateFlow
7−import kotlinx.coroutines.launch
8−import soy.iko.opencode.CrashLogger
9−
10−/**
11− * The bridge between the Crux core (Rust or pure-Kotlin fallback) and the
12− * Jetpack Compose UI.
13− *
14− * - Sends [Event]s to the core via [update]
15− * - Processes [Effect]s returned by the core (Render, Http)
16− * - Exposes the current [ViewModel] as a [StateFlow] for Compose to observe
17− */
18−class Core : androidx.lifecycle.ViewModel() {
19− private val ffi: CoreFfi = CoreFfi.create()
20− private val httpClient = HttpClient()
21− private val sseClient = SseClient()
22−
23− private val _view = MutableStateFlow(getInitialView())
24− val view: StateFlow<ViewModel> = _view.asStateFlow()
25−
26− init {
27− // Emit Start event to initialize the model
28− update(Event.Start)
29−
30− // Subscribe to SSE events
31− viewModelScope.launch {
32− sseClient.events.collect { data ->
33− update(Event.EventReceived(data))
34− }
35− }
36− }
37−
38− fun update(event: Event) {
39− val effectsBytes = ffi.update(event.bincodeSerialize())
40− val requests = Requests.bincodeDeserialize(effectsBytes).value
41− for (request in requests) {
42− processEffect(request)
43− }
44− }
45−
46− private fun processEffect(request: Request) {
47− when (val effect = request.effect) {
48− is Effect.Render -> {
49− _view.value = ViewModel.bincodeDeserialize(ffi.view())
50− }
51− is Effect.Http -> {
52− handleHttpEffect(effect.value, request.id)
53− }
54− }
55− }
56−
57− private fun handleHttpEffect(request: HttpRequest, requestId: UInt) {
58− viewModelScope.launch {
59− val result = httpClient.request(request)
60− val effectsBytes = ffi.resolve(requestId, result.bincodeSerialize())
61− val requests = Requests.bincodeDeserialize(effectsBytes).value
62− for (req in requests) {
63− processEffect(req)
64− }
65− // Refresh the view after resolving
66− _view.value = ViewModel.bincodeDeserialize(ffi.view())
67−
68− // Connect SSE when we have a server URL and are connected
69− val currentView = _view.value
70− if (currentView.connected && currentView.currentSessionId != null) {
71− sseClient.connect("${currentView.serverUrl}/event")
72− }
73− }
74− }
75−
76− private fun getInitialView(): ViewModel {
77− return try {
78− ViewModel.bincodeDeserialize(ffi.view())
79− } catch (e: Throwable) {
80− CrashLogger.report(e, "Core.getInitialView")
81− ViewModel(
82− screen = Screen.CONNECT,
83− serverUrl = "http://localhost:4096",
84− connected = false,
85− loading = false,
86− error = null,
87− sessions = emptyList(),
88− currentSessionId = null,
89− messages = emptyList(),
90− draftMessage = "",
91− )
92− }
93− }
94−
95− override fun onCleared() {
96− sseClient.disconnect()
97− }
98−}
app/src/main/java/soy/iko/opencode/core/CoreFfi.ktdeleted−44
@@ -1,44 +0,0 @@
1−package soy.iko.opencode.core
2−
3−/**
4− * FFI interface to the Rust Crux core.
5− *
6− * When the native library [libopencode.so] is available, [NativeCoreFfi] is used.
7− * Otherwise [PureCoreFfi] provides a pure-Kotlin reimplementation for development.
8− */
9−interface CoreFfi {
10− fun update(data: ByteArray): ByteArray
11− fun resolve(id: UInt, data: ByteArray): ByteArray
12− fun view(): ByteArray
13−
14− companion object {
15− fun create(): CoreFfi {
16− return try {
17− NativeCoreFfi()
18− } catch (e: Throwable) {
19− PureCoreFfi()
20− }
21− }
22− }
23−}
24−
25−/**
26− * JNI bridge to the Rust [libopencode.so] native library.
27− * Generated by BoltFFI.
28− */
29−private class NativeCoreFfi : CoreFfi {
30− init {
31− System.loadLibrary("opencode")
32− }
33−
34− private external fun nativeNew(): Long
35− private external fun nativeUpdate(ptr: Long, data: ByteArray): ByteArray
36− private external fun nativeResolve(ptr: Long, id: Int, data: ByteArray): ByteArray
37− private external fun nativeView(ptr: Long): ByteArray
38−
39− private val ptr: Long = nativeNew()
40−
41− override fun update(data: ByteArray): ByteArray = nativeUpdate(ptr, data)
42− override fun resolve(id: UInt, data: ByteArray): ByteArray = nativeResolve(ptr, id.toInt(), data)
43− override fun view(): ByteArray = nativeView(ptr)
44−}
app/src/main/java/soy/iko/opencode/core/HttpClient.ktdeleted−74
@@ -1,74 +0,0 @@
1−package soy.iko.opencode.core
2−
3−import com.novi.serde.Bytes
4−import io.ktor.client.HttpClient as KtorHttpClient
5−import io.ktor.client.call.body
6−import io.ktor.client.engine.okhttp.OkHttp
7−import io.ktor.client.plugins.HttpTimeout
8−import io.ktor.client.request.headers
9−import io.ktor.client.request.request
10−import io.ktor.client.request.setBody
11−import io.ktor.client.statement.bodyAsText
12−import io.ktor.http.HttpMethod
13−import io.ktor.util.flattenEntries
14−import kotlinx.coroutines.Dispatchers
15−import kotlinx.coroutines.withContext
16−import java.net.SocketTimeoutException
17−import java.net.UnknownHostException
18−import kotlin.coroutines.cancellation.CancellationException
19−
20−/**
21− * Executes HTTP requests on behalf of the Crux core.
22− *
23− * When the core emits an [Effect.Http], the shell calls [request] with the
24− * [HttpRequest] and returns the [HttpResult] back to the core via [Core.resolve].
25− */
26−class HttpClient {
27− private val ktorHttpClient = KtorHttpClient(OkHttp) {
28− install(HttpTimeout) {
29− requestTimeoutMillis = 30_000
30− connectTimeoutMillis = 15_000
31− socketTimeoutMillis = 15_000
32− }
33− }
34−
35− suspend fun request(request: HttpRequest): HttpResult = withContext(Dispatchers.IO) {
36− try {
37− val response = executeRequest(request)
38− HttpResult.Ok(response)
39− } catch (ce: CancellationException) {
40− throw ce
41− } catch (e: Throwable) {
42− HttpResult.Err(toHttpError(e))
43− }
44− }
45−
46− private suspend fun executeRequest(request: HttpRequest): HttpResponse {
47− val response = ktorHttpClient.request(request.url) {
48− this.method = HttpMethod.parse(request.method)
49− this.headers {
50− for (header in request.headers) {
51− append(header.name, header.value)
52− }
53− }
54− if (request.body.content.isNotEmpty()) {
55− setBody(request.body.content)
56− }
57− }
58− val bytes: ByteArray = response.body()
59− val headers = response.headers
60− .flattenEntries()
61− .map { HttpHeader(it.first, it.second) }
62− return HttpResponse(
63− status = response.status.value.toUShort(),
64− headers = headers,
65− body = Bytes(bytes),
66− )
67− }
68−
69− private fun toHttpError(error: Throwable): HttpError = when (error) {
70− is SocketTimeoutException -> HttpError.Timeout
71− is UnknownHostException -> HttpError.Io("Unknown host")
72− else -> HttpError.Io(error.message ?: "HTTP request failed")
73− }
74−}
app/src/main/java/soy/iko/opencode/core/PureCoreFfi.ktdeleted−347
@@ -1,347 +0,0 @@
1−package soy.iko.opencode.core
2−
3−import com.novi.serde.Bytes
4−
5−/**
6− * Pure-Kotlin fallback implementation of the Crux core.
7− *
8− * This mirrors the Rust [app.rs] logic so the app can run during development
9− * without the compiled native library. When [libopencode.so] is available,
10− * [NativeCoreFfi] is used instead and this class is never instantiated.
11− */
12−internal class PureCoreFfi : CoreFfi {
13− private val model = PureModel()
14−
15− override fun update(data: ByteArray): ByteArray {
16− val event = Event.bincodeDeserialize(data)
17− val effects = model.handleEvent(event)
18− return effects.bincodeSerialize()
19− }
20−
21− override fun resolve(id: UInt, data: ByteArray): ByteArray {
22− val result = HttpResult.bincodeDeserialize(data)
23− val effects = model.handleResult(id, result)
24− return effects.bincodeSerialize()
25− }
26−
27− override fun view(): ByteArray {
28− return model.currentView().bincodeSerialize()
29− }
30−}
31−
32−internal data class PureModel(
33− var serverUrl: String = "http://localhost:4096",
34− var connected: Boolean = false,
35− var loading: Boolean = false,
36− var error: String? = null,
37− var sessions: MutableList<PureSession> = mutableListOf(),
38− var currentSessionId: String? = null,
39− var messages: MutableList<PureMessage> = mutableListOf(),
40− var draftMessage: String = "",
41− var pendingRequests: MutableMap<UInt, PendingRequest> = mutableMapOf(),
42− var nextId: UInt = 0u,
43−) {
44− fun handleEvent(event: Event): Requests {
45− val requests = mutableListOf<Request>()
46− when (event) {
47− is Event.Start -> {
48− serverUrl = "http://localhost:4096"
49− }
50− is Event.ServerUrlChanged -> {
51− serverUrl = event.value
52− }
53− is Event.Connect -> {
54− loading = true
55− error = null
56− val id = nextId++
57− pendingRequests[id] = PendingRequest.Connect
58− requests.add(
59− Request(id, Effect.Http(makeGetRequest("${serverUrl}/global/health"))),
60− )
61− }
62− is Event.LoadSessions -> {
63− loading = true
64− val id = nextId++
65− pendingRequests[id] = PendingRequest.LoadSessions
66− requests.add(
67− Request(id, Effect.Http(makeGetRequest("${serverUrl}/session"))),
68− )
69− }
70− is Event.SelectSession -> {
71− currentSessionId = event.value
72− messages.clear()
73− val id = nextId++
74− pendingRequests[id] = PendingRequest.LoadMessages(event.value)
75− requests.add(
76− Request(id, Effect.Http(makeGetRequest("${serverUrl}/session/${event.value}/message"))),
77− )
78− }
79− is Event.CreateSession -> {
80− loading = true
81− val id = nextId++
82− pendingRequests[id] = PendingRequest.CreateSession
83− requests.add(
84− Request(id, Effect.Http(makePostRequest("${serverUrl}/session", ByteArray(0))),
85− ))
86− }
87− is Event.LoadMessages -> {
88− loading = true
89− val id = nextId++
90− pendingRequests[id] = PendingRequest.LoadMessages(event.value)
91− requests.add(
92− Request(id, Effect.Http(makeGetRequest("${serverUrl}/session/${event.value}/message"))),
93− )
94− }
95− is Event.SendMessage -> {
96− val sessionId = currentSessionId ?: return Requests(requests)
97− draftMessage = ""
98− val body = """{"sessionID":"$sessionId","parts":[{"type":"text","text":"${event.value}"}]}"""
99− .toByteArray()
100− val id = nextId++
101− pendingRequests[id] = PendingRequest.SendMessage(sessionId)
102− requests.add(
103− Request(
104− id,
105− Effect.Http(
106− makePostRequest("${serverUrl}/session/$sessionId/prompt_async", body)
107− .copy(headers = listOf(HttpHeader("Content-Type", "application/json"))),
108− ),
109− ),
110− )
111− }
112− is Event.EventReceived -> {
113− val sid = currentSessionId
114− if (sid != null) {
115− val id = nextId++
116− pendingRequests[id] = PendingRequest.LoadMessages(sid)
117− requests.add(
118− Request(id, Effect.Http(makeGetRequest("${serverUrl}/session/$sid/message"))),
119− )
120− }
121− }
122− is Event.NavigateToChat -> {
123− currentSessionId = event.value
124− messages.clear()
125− }
126− is Event.NavigateToSessions -> {
127− currentSessionId = null
128− messages.clear()
129− }
130− is Event.DismissError -> {
131− error = null
132− }
133− else -> {}
134− }
135− return Requests(requests)
136− }
137−
138− fun handleResult(id: UInt, result: HttpResult): Requests {
139− val requests = mutableListOf<Request>()
140− val pending = pendingRequests.remove(id) ?: return Requests(requests)
141−
142− when (pending) {
143− is PendingRequest.Connect -> {
144− when (result) {
145− is HttpResult.Ok -> {
146− connected = true
147− loading = false
148− // Trigger LoadSessions
149− val newId = nextId++
150− pendingRequests[newId] = PendingRequest.LoadSessions
151− requests.add(
152− Request(newId, Effect.Http(makeGetRequest("${serverUrl}/session"))),
153− )
154− }
155− is HttpResult.Err -> {
156− loading = false
157− error = "Connection failed: ${result.value}"
158− }
159− }
160− }
161− is PendingRequest.LoadSessions -> {
162− loading = false
163− when (result) {
164− is HttpResult.Ok -> {
165− val body = result.value.body.content.decodeToString()
166− sessions = parseSessions(body).toMutableList()
167− connected = true
168− }
169− is HttpResult.Err -> {
170− error = "Failed to load sessions: ${result.value}"
171− }
172− }
173− }
174− is PendingRequest.CreateSession -> {
175− loading = false
176− when (result) {
177− is HttpResult.Ok -> {
178− val body = result.value.body.content.decodeToString()
179− val sid = extractJsonString(body, "id") ?: ""
180− if (sid.isNotEmpty()) {
181− currentSessionId = sid
182− messages.clear()
183− val newId = nextId++
184− pendingRequests[newId] = PendingRequest.LoadMessages(sid)
185− requests.add(
186− Request(newId, Effect.Http(makeGetRequest("${serverUrl}/session/$sid/message"))),
187− )
188− }
189− }
190− is HttpResult.Err -> {
191− error = "Failed to create session: ${result.value}"
192− }
193− }
194− }
195− is PendingRequest.LoadMessages -> {
196− loading = false
197− when (result) {
198− is HttpResult.Ok -> {
199− val body = result.value.body.content.decodeToString()
200− messages = parseMessages(body).toMutableList()
201− }
202− is HttpResult.Err -> {
203− error = "Failed to load messages: ${result.value}"
204− }
205− }
206− }
207− is PendingRequest.SendMessage -> {
208− when (result) {
209− is HttpResult.Ok -> {
210− val sid = pending.sessionId
211− val newId = nextId++
212− pendingRequests[newId] = PendingRequest.LoadMessages(sid)
213− requests.add(
214− Request(newId, Effect.Http(makeGetRequest("${serverUrl}/session/$sid/message"))),
215− )
216− }
217− is HttpResult.Err -> {
218− error = "Failed to send message: ${result.value}"
219− }
220− }
221− }
222− }
223− return Requests(requests)
224− }
225−
226− fun currentView(): ViewModel {
227− val screen = when {
228− currentSessionId != null -> Screen.CHAT
229− connected -> Screen.SESSIONS
230− else -> Screen.CONNECT
231− }
232− return ViewModel(
233− screen = screen,
234− serverUrl = serverUrl,
235− connected = connected,
236− loading = loading,
237− error = error,
238− sessions = sessions.map { SessionView(it.id, it.title) },
239− currentSessionId = currentSessionId,
240− messages = messages.map { MessageView(it.id, it.role, it.text, it.time) },
241− draftMessage = draftMessage,
242− )
243− }
244−
245− private fun makeGetRequest(url: String): HttpRequest =
246− HttpRequest(
247− method = "GET",
248− url = url,
249− headers = emptyList(),
250− body = Bytes(ByteArray(0)),
251− )
252−
253− private fun makePostRequest(url: String, body: ByteArray): HttpRequest =
254− HttpRequest(
255− method = "POST",
256− url = url,
257− headers = emptyList(),
258− body = Bytes(body),
259− )
260−}
261−
262−internal sealed class PendingRequest {
263− data object Connect : PendingRequest()
264− data object LoadSessions : PendingRequest()
265− data object CreateSession : PendingRequest()
266− data class LoadMessages(val sessionId: String) : PendingRequest()
267− data class SendMessage(val sessionId: String) : PendingRequest()
268−}
269−
270−internal data class PureSession(val id: String, val title: String)
271−
272−internal data class PureMessage(val id: String, val role: String, val text: String, val time: ULong)
273−
274−internal fun parseSessions(json: String): List<PureSession> {
275− val sessions = mutableListOf<PureSession>()
276− var depth = 0
277− var start = 0
278− for (i in json.indices) {
279− when (json[i]) {
280− '{' -> {
281− if (depth == 0) start = i
282− depth++
283− }
284− '}' -> {
285− depth--
286− if (depth == 0) {
287− val obj = json.substring(start, i + 1)
288− val id = extractJsonString(obj, "id") ?: ""
289− val title = extractJsonString(obj, "title") ?: ""
290− if (id.isNotEmpty()) {
291− sessions.add(PureSession(id, if (title.isNotEmpty()) title else "Untitled"))
292− }
293− }
294− }
295− }
296− }
297− return sessions
298−}
299−
300−internal fun parseMessages(json: String): List<PureMessage> {
301− val messages = mutableListOf<PureMessage>()
302− var depth = 0
303− var start = 0
304− for (i in json.indices) {
305− when (json[i]) {
306− '{' -> {
307− if (depth == 0) start = i
308− depth++
309− }
310− '}' -> {
311− depth--
312− if (depth == 0) {
313− val obj = json.substring(start, i + 1)
314− val id = extractJsonString(obj, "id") ?: ""
315− val role = extractJsonString(obj, "role") ?: ""
316− val time = extractJsonNumber(obj, "created") ?: 0uL
317− if (id.isNotEmpty() && role.isNotEmpty()) {
318− messages.add(PureMessage(id, role, "", time))
319− }
320− }
321− }
322− }
323− }
324− return messages
325−}
326−
327−internal fun extractJsonString(json: String, key: String): String? {
328− val pattern = "\"$key\""
329− val idx = json.indexOf(pattern) ?: return null
330− val rest = json.substring(idx + pattern.length)
331− val colon = rest.indexOf(':') ?: return null
332− val rest2 = rest.substring(colon + 1)
333− val quote = rest2.indexOf('"') ?: return null
334− val rest3 = rest2.substring(quote + 1)
335− val end = rest3.indexOf('"') ?: return null
336− return rest3.substring(0, end)
337−}
338−
339−internal fun extractJsonNumber(json: String, key: String): ULong? {
340− val pattern = "\"$key\""
341− val idx = json.indexOf(pattern) ?: return null
342− val rest = json.substring(idx + pattern.length)
343− val colon = rest.indexOf(':') ?: return null
344− val rest2 = rest.substring(colon + 1).trimStart()
345− val end = rest2.indexOfFirst { !it.isDigit() }.let { if (it == -1) rest2.length else it }
346− return rest2.substring(0, end).toULongOrNull()
347−}
app/src/main/java/soy/iko/opencode/core/SseClient.ktdeleted−67
@@ -1,67 +0,0 @@
1−package soy.iko.opencode.core
2−
3−import io.ktor.client.HttpClient
4−import io.ktor.client.engine.okhttp.OkHttp
5−import io.ktor.client.plugins.HttpTimeout
6−import io.ktor.client.request.prepareGet
7−import io.ktor.client.statement.bodyAsChannel
8−import io.ktor.utils.io.readUTF8Line
9−import kotlinx.coroutines.CoroutineScope
10−import kotlinx.coroutines.Dispatchers
11−import kotlinx.coroutines.Job
12−import kotlinx.coroutines.SupervisorJob
13−import kotlinx.coroutines.flow.MutableSharedFlow
14−import kotlinx.coroutines.flow.SharedFlow
15−import kotlinx.coroutines.flow.asSharedFlow
16−import kotlinx.coroutines.launch
17−import soy.iko.opencode.CrashLogger
18−
19−/**
20− * Subscribes to the opencode server's SSE event stream.
21− *
22− * Events are emitted as raw strings on [events]. The Core converts them into
23− * [Event.EventReceived] which triggers a message reload.
24− */
25−class SseClient {
26− private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
27− private val httpClient = HttpClient(OkHttp) {
28− install(HttpTimeout) {
29− requestTimeoutMillis = Long.MAX_VALUE
30− connectTimeoutMillis = 15_000
31− socketTimeoutMillis = Long.MAX_VALUE
32− }
33− }
34−
35− private val _events = MutableSharedFlow<String>(extraBufferCapacity = 64)
36− val events: SharedFlow<String> = _events.asSharedFlow()
37−
38− private var currentJob: Job? = null
39−
40− fun connect(url: String) {
41− currentJob?.cancel()
42− currentJob = scope.launch {
43− try {
44− httpClient.prepareGet(url).execute { response ->
45− val channel = response.bodyAsChannel()
46− val dataBuilder = StringBuilder()
47− while (!channel.isClosedForRead) {
48− val line = channel.readUTF8Line() ?: break
49− if (line.startsWith("data: ")) {
50− dataBuilder.append(line.substring(6))
51− } else if (line.isEmpty() && dataBuilder.isNotEmpty()) {
52− _events.emit(dataBuilder.toString())
53− dataBuilder.clear()
54− }
55− }
56− }
57− } catch (e: Throwable) {
58− CrashLogger.report(e, "SseClient")
59− }
60− }
61− }
62−
63− fun disconnect() {
64− currentJob?.cancel()
65− currentJob = null
66− }
67−}
app/src/main/java/soy/iko/opencode/ui/screens/ChatScreen.ktdeleted−212
@@ -1,212 +0,0 @@
1−package soy.iko.opencode.ui.screens
2−
3−import androidx.compose.foundation.layout.Arrangement
4−import androidx.compose.foundation.layout.Box
5−import androidx.compose.foundation.layout.Column
6−import androidx.compose.foundation.layout.Row
7−import androidx.compose.foundation.layout.Spacer
8−import androidx.compose.foundation.layout.fillMaxSize
9−import androidx.compose.foundation.layout.fillMaxWidth
10−import androidx.compose.foundation.layout.height
11−import androidx.compose.foundation.layout.padding
12−import androidx.compose.foundation.layout.size
13−import androidx.compose.foundation.layout.widthIn
14−import androidx.compose.foundation.lazy.LazyColumn
15−import androidx.compose.foundation.lazy.items
16−import androidx.compose.foundation.lazy.rememberLazyListState
17−import androidx.compose.foundation.shape.RoundedCornerShape
18−import androidx.compose.material.icons.Icons
19−import androidx.compose.material.icons.automirrored.filled.ArrowBack
20−import androidx.compose.material.icons.automirrored.filled.Send
21−import androidx.compose.material3.CircularProgressIndicator
22−import androidx.compose.material3.ExperimentalMaterial3Api
23−import androidx.compose.material3.Icon
24−import androidx.compose.material3.IconButton
25−import androidx.compose.material3.MaterialTheme
26−import androidx.compose.material3.OutlinedTextField
27−import androidx.compose.material3.Scaffold
28−import androidx.compose.material3.Surface
29−import androidx.compose.material3.Text
30−import androidx.compose.material3.TopAppBar
31−import androidx.compose.runtime.Composable
32−import androidx.compose.runtime.LaunchedEffect
33−import androidx.compose.runtime.collectAsState
34−import androidx.compose.runtime.getValue
35−import androidx.compose.runtime.mutableStateOf
36−import androidx.compose.runtime.remember
37−import androidx.compose.runtime.setValue
38−import androidx.compose.ui.Alignment
39−import androidx.compose.ui.Modifier
40−import androidx.compose.ui.draw.clip
41−import androidx.compose.ui.unit.dp
42−import soy.iko.opencode.core.Core
43−import soy.iko.opencode.core.Event
44−
45−@OptIn(ExperimentalMaterial3Api::class)
46−@Composable
47−fun ChatScreen(core: Core) {
48− val view by core.view.collectAsState()
49− var inputText by remember { mutableStateOf("") }
50− val listState = rememberLazyListState()
51−
52− LaunchedEffect(view.messages.size) {
53− if (view.messages.isNotEmpty()) {
54− listState.animateScrollToItem(view.messages.size - 1)
55− }
56− }
57−
58− Scaffold(
59− topBar = {
60− TopAppBar(
61− title = {
62− Text(
63− view.currentSessionId?.take(8)?.plus("…") ?: "Chat",
64− maxLines = 1,
65− )
66− },
67− navigationIcon = {
68− IconButton(onClick = { core.update(Event.NavigateToSessions) }) {
69− Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
70− }
71− },
72− )
73− },
74− bottomBar = {
75− ChatInputBar(
76− text = inputText,
77− onTextChange = { inputText = it },
78− onSend = {
79− if (inputText.isNotBlank()) {
80− core.update(Event.SendMessage(inputText))
81− inputText = ""
82− }
83− },
84− enabled = !view.loading,
85− )
86− },
87− ) { padding ->
88− Box(
89− modifier = Modifier
90− .fillMaxSize()
91− .padding(padding),
92− ) {
93− if (view.loading && view.messages.isEmpty()) {
94− CircularProgressIndicator(
95− modifier = Modifier.align(Alignment.Center),
96− )
97− } else if (view.messages.isEmpty()) {
98− Column(
99− horizontalAlignment = Alignment.CenterHorizontally,
100− verticalArrangement = Arrangement.Center,
101− modifier = Modifier.fillMaxSize().padding(24.dp),
102− ) {
103− Text(
104− "Send a message to start chatting",
105− style = MaterialTheme.typography.bodyLarge,
106− color = MaterialTheme.colorScheme.onSurfaceVariant,
107− )
108− }
109− } else {
110− LazyColumn(
111− state = listState,
112− modifier = Modifier.fillMaxSize().padding(horizontal = 12.dp),
113− verticalArrangement = Arrangement.spacedBy(8.dp),
114− contentPadding = androidx.compose.foundation.layout.PaddingValues(vertical = 12.dp),
115− ) {
116− items(view.messages, key = { it.id }) { message ->
117− MessageBubble(message)
118− }
119− }
120− }
121− }
122− }
123−}
124−
125−@Composable
126−private fun MessageBubble(message: soy.iko.opencode.core.MessageView) {
127− val isUser = message.role == "user"
128− Row(
129− modifier = Modifier.fillMaxWidth(),
130− horizontalArrangement = if (isUser) Arrangement.End else Arrangement.Start,
131− ) {
132− Surface(
133− color = if (isUser) {
134− MaterialTheme.colorScheme.primary
135− } else {
136− MaterialTheme.colorScheme.surfaceVariant
137− },
138− contentColor = if (isUser) {
139− MaterialTheme.colorScheme.onPrimary
140− } else {
141− MaterialTheme.colorScheme.onSurfaceVariant
142− },
143− shape = RoundedCornerShape(
144− topStart = 16.dp,
145− topEnd = 16.dp,
146− bottomStart = if (isUser) 16.dp else 4.dp,
147− bottomEnd = if (isUser) 4.dp else 16.dp,
148− ),
149− modifier = Modifier.widthIn(max = 280.dp),
150− ) {
151− Column(
152− modifier = Modifier.padding(horizontal = 14.dp, vertical = 10.dp),
153− ) {
154− if (!isUser) {
155− Text(
156− text = message.role.replaceFirstChar { it.uppercase() },
157− style = MaterialTheme.typography.labelSmall,
158− color = MaterialTheme.colorScheme.primary,
159− )
160− Spacer(Modifier.height(2.dp))
161− }
162− Text(
163− text = message.text.ifBlank { "(no content)" },
164− style = MaterialTheme.typography.bodyMedium,
165− )
166− }
167− }
168− }
169−}
170−
171−@Composable
172−private fun ChatInputBar(
173− text: String,
174− onTextChange: (String) -> Unit,
175− onSend: () -> Unit,
176− enabled: Boolean,
177−) {
178− Surface(
179− tonalElevation = 3.dp,
180− modifier = Modifier.fillMaxWidth(),
181− ) {
182− Row(
183− verticalAlignment = Alignment.CenterVertically,
184− modifier = Modifier
185− .fillMaxWidth()
186− .padding(horizontal = 12.dp, vertical = 8.dp),
187− ) {
188− OutlinedTextField(
189− value = text,
190− onValueChange = onTextChange,
191− placeholder = { Text("Message…") },
192− modifier = Modifier.weight(1f),
193− maxLines = 4,
194− )
195− Spacer(Modifier.size(8.dp))
196− IconButton(
197− onClick = onSend,
198− enabled = enabled && text.isNotBlank(),
199− ) {
200− Icon(
201− Icons.AutoMirrored.Filled.Send,
202− contentDescription = "Send",
203− tint = if (text.isNotBlank()) {
204− MaterialTheme.colorScheme.primary
205− } else {
206− MaterialTheme.colorScheme.onSurfaceVariant
207− },
208− )
209− }
210− }
211− }
212−}
app/src/main/java/soy/iko/opencode/ui/screens/ConnectScreen.ktdeleted−109
@@ -1,109 +0,0 @@
1−package soy.iko.opencode.ui.screens
2−
3−import androidx.compose.foundation.layout.Arrangement
4−import androidx.compose.foundation.layout.Column
5−import androidx.compose.foundation.layout.Spacer
6−import androidx.compose.foundation.layout.fillMaxSize
7−import androidx.compose.foundation.layout.height
8−import androidx.compose.foundation.layout.padding
9−import androidx.compose.foundation.layout.size
10−import androidx.compose.foundation.text.KeyboardOptions
11−import androidx.compose.material.icons.Icons
12−import androidx.compose.material.icons.filled.Dns
13−import androidx.compose.material3.Button
14−import androidx.compose.material3.CircularProgressIndicator
15−import androidx.compose.material3.ExperimentalMaterial3Api
16−import androidx.compose.material3.Icon
17−import androidx.compose.material3.MaterialTheme
18−import androidx.compose.material3.OutlinedTextField
19−import androidx.compose.material3.Scaffold
20−import androidx.compose.material3.SnackbarHost
21−import androidx.compose.material3.SnackbarHostState
22−import androidx.compose.material3.Text
23−import androidx.compose.material3.TopAppBar
24−import androidx.compose.runtime.Composable
25−import androidx.compose.runtime.LaunchedEffect
26−import androidx.compose.runtime.collectAsState
27−import androidx.compose.runtime.getValue
28−import androidx.compose.runtime.mutableStateOf
29−import androidx.compose.runtime.remember
30−import androidx.compose.runtime.setValue
31−import androidx.compose.ui.Alignment
32−import androidx.compose.ui.Modifier
33−import androidx.compose.ui.text.input.ImeAction
34−import androidx.compose.ui.text.input.KeyboardType
35−import androidx.compose.ui.unit.dp
36−import soy.iko.opencode.core.Core
37−import soy.iko.opencode.core.Event
38−
39−@OptIn(ExperimentalMaterial3Api::class)
40−@Composable
41−fun ConnectScreen(core: Core) {
42− val view by core.view.collectAsState()
43− var url by remember(view.serverUrl) { mutableStateOf(view.serverUrl) }
44− val snackbarHostState = remember { SnackbarHostState() }
45−
46− LaunchedEffect(view.error) {
47− view.error?.let {
48− snackbarHostState.showSnackbar(it)
49− }
50− }
51−
52− Scaffold(
53− topBar = {
54− TopAppBar(title = { Text("Opencode") })
55− },
56− snackbarHost = { SnackbarHost(snackbarHostState) },
57− ) { padding ->
58− Column(
59− horizontalAlignment = Alignment.CenterHorizontally,
60− verticalArrangement = Arrangement.Center,
61− modifier = Modifier
62− .fillMaxSize()
63− .padding(padding)
64− .padding(24.dp),
65− ) {
66− Icon(
67− imageVector = Icons.Default.Dns,
68− contentDescription = null,
69− modifier = Modifier.size(72.dp),
70− tint = MaterialTheme.colorScheme.primary,
71− )
72− Spacer(Modifier.height(16.dp))
73− Text(
74− text = "Connect to Opencode Server",
75− style = MaterialTheme.typography.headlineSmall,
76− )
77− Spacer(Modifier.height(24.dp))
78− OutlinedTextField(
79− value = url,
80− onValueChange = { url = it },
81− label = { Text("Server URL") },
82− singleLine = true,
83− keyboardOptions = KeyboardOptions(
84− keyboardType = KeyboardType.Uri,
85− imeAction = ImeAction.Go,
86− ),
87− modifier = Modifier.padding(horizontal = 16.dp),
88− )
89− Spacer(Modifier.height(16.dp))
90− Button(
91− onClick = {
92− core.update(Event.ServerUrlChanged(url))
93− core.update(Event.Connect)
94− },
95− enabled = !view.loading,
96− ) {
97− if (view.loading) {
98− CircularProgressIndicator(
99− modifier = Modifier.size(20.dp),
100− strokeWidth = 2.dp,
101− color = MaterialTheme.colorScheme.onPrimary,
102− )
103− } else {
104− Text("Connect")
105− }
106− }
107− }
108− }
109−}
app/src/main/java/soy/iko/opencode/ui/screens/SessionsScreen.ktdeleted−101
@@ -1,101 +0,0 @@
1−package soy.iko.opencode.ui.screens
2−
3−import androidx.compose.foundation.clickable
4−import androidx.compose.foundation.layout.Box
5−import androidx.compose.foundation.layout.Column
6−import androidx.compose.foundation.layout.fillMaxSize
7−import androidx.compose.foundation.layout.padding
8−import androidx.compose.foundation.lazy.LazyColumn
9−import androidx.compose.foundation.lazy.items
10−import androidx.compose.material.icons.Icons
11−import androidx.compose.material.icons.automirrored.filled.ArrowBack
12−import androidx.compose.material.icons.filled.Add
13−import androidx.compose.material.icons.filled.ChatBubble
14−import androidx.compose.material3.CircularProgressIndicator
15−import androidx.compose.material3.ExperimentalMaterial3Api
16−import androidx.compose.material3.FloatingActionButton
17−import androidx.compose.material3.Icon
18−import androidx.compose.material3.IconButton
19−import androidx.compose.material3.ListItem
20−import androidx.compose.material3.MaterialTheme
21−import androidx.compose.material3.Scaffold
22−import androidx.compose.material3.Text
23−import androidx.compose.material3.TopAppBar
24−import androidx.compose.runtime.Composable
25−import androidx.compose.runtime.collectAsState
26−import androidx.compose.runtime.getValue
27−import androidx.compose.ui.Alignment
28−import androidx.compose.ui.Modifier
29−import androidx.compose.ui.unit.dp
30−import soy.iko.opencode.core.Core
31−import soy.iko.opencode.core.Event
32−
33−@OptIn(ExperimentalMaterial3Api::class)
34−@Composable
35−fun SessionsScreen(core: Core) {
36− val view by core.view.collectAsState()
37−
38− Scaffold(
39− topBar = {
40− TopAppBar(
41− title = { Text("Sessions") },
42− actions = {
43− IconButton(onClick = { core.update(Event.CreateSession) }) {
44− Icon(Icons.Default.Add, contentDescription = "New session")
45− }
46− },
47− )
48− },
49− floatingActionButton = {
50− FloatingActionButton(onClick = { core.update(Event.CreateSession) }) {
51− Icon(Icons.Default.Add, contentDescription = "New session")
52− }
53− },
54− ) { padding ->
55− Box(
56− modifier = Modifier
57− .fillMaxSize()
58− .padding(padding),
59− ) {
60− if (view.loading && view.sessions.isEmpty()) {
61− CircularProgressIndicator(
62− modifier = Modifier.align(Alignment.Center),
63− )
64− } else if (view.sessions.isEmpty()) {
65− Column(
66− horizontalAlignment = Alignment.CenterHorizontally,
67− modifier = Modifier
68− .fillMaxSize()
69− .padding(24.dp),
70− ) {
71− Text(
72− "No sessions yet",
73− style = MaterialTheme.typography.bodyLarge,
74− color = MaterialTheme.colorScheme.onSurfaceVariant,
75− )
76− }
77− } else {
78− LazyColumn {
79− items(view.sessions, key = { it.id }) { session ->
80− ListItem(
81− headlineContent = { Text(session.title.ifEmpty { "Untitled" }) },
82− supportingContent = { Text(session.id.take(8)) },
83− leadingContent = {
84− Icon(
85− Icons.Default.ChatBubble,
86− contentDescription = null,
87− tint = MaterialTheme.colorScheme.primary,
88− )
89− },
90− modifier = Modifier
91− .padding(horizontal = 0.dp)
92− .clickable {
93− core.update(Event.SelectSession(session.id))
94− },
95− )
96− }
97− }
98− }
99− }
100− }
101−}
app/src/main/java/soy/iko/opencode/ui/theme/Theme.ktdeleted−58
@@ -1,58 +0,0 @@
1−package soy.iko.opencode.ui.theme
2−
3−import android.os.Build
4−import androidx.compose.foundation.isSystemInDarkTheme
5−import androidx.compose.material3.MaterialTheme
6−import androidx.compose.material3.darkColorScheme
7−import androidx.compose.material3.dynamicDarkColorScheme
8−import androidx.compose.material3.dynamicLightColorScheme
9−import androidx.compose.material3.lightColorScheme
10−import androidx.compose.runtime.Composable
11−import androidx.compose.ui.graphics.Color
12−import androidx.compose.ui.platform.LocalContext
13−
14−private val DarkColorScheme = darkColorScheme(
15− primary = Color(0xFF82B1FF),
16− secondary = Color(0xFF80CBC4),
17− tertiary = Color(0xFFFFB74D),
18− background = Color(0xFF121212),
19− surface = Color(0xFF1E1E1E),
20− onPrimary = Color.Black,
21− onSecondary = Color.Black,
22− onTertiary = Color.Black,
23− onBackground = Color.White,
24− onSurface = Color.White,
25−)
26−
27−private val LightColorScheme = lightColorScheme(
28− primary = Color(0xFF1565C0),
29− secondary = Color(0xFF00897B),
30− tertiary = Color(0xFFF57C00),
31− background = Color(0xFFF5F5F5),
32− surface = Color.White,
33− onPrimary = Color.White,
34− onSecondary = Color.White,
35− onTertiary = Color.White,
36− onBackground = Color.Black,
37− onSurface = Color.Black,
38−)
39−
40−@Composable
41−fun OpencodeTheme(
42− darkTheme: Boolean = isSystemInDarkTheme(),
43− dynamicColor: Boolean = true,
44− content: @Composable () -> Unit,
45−) {
46− val colorScheme = when {
47− dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
48− val context = LocalContext.current
49− if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
50− }
51− darkTheme -> DarkColorScheme
52− else -> LightColorScheme
53− }
54− MaterialTheme(
55− colorScheme = colorScheme,
56− content = content,
57− )
58−}
app/src/main/res/values-night/themes.xmldeleted−4
@@ -1,4 +0,0 @@
1−<?xml version="1.0" encoding="utf-8"?>
2−<resources>
3− <style name="Theme.Opencode" parent="android:Theme.Material.NoActionBar" />
4−</resources>
app/src/main/res/values/strings.xmldeleted−3
@@ -1,3 +0,0 @@
1−<resources>
2− <string name="app_name">Opencode</string>
3−</resources>
app/src/main/res/values/themes.xmldeleted−4
@@ -1,4 +0,0 @@
1−<?xml version="1.0" encoding="utf-8"?>
2−<resources>
3− <style name="Theme.Opencode" parent="android:Theme.Material.Light.NoActionBar" />
4−</resources>
app/src/test/java/soy/iko/opencode/core/PureCoreFfiTest.ktdeleted−136
@@ -1,136 +0,0 @@
1−package soy.iko.opencode.core
2−
3−import org.junit.Assert.assertEquals
4−import org.junit.Assert.assertFalse
5−import org.junit.Assert.assertNotNull
6−import org.junit.Assert.assertNull
7−import org.junit.Assert.assertTrue
8−import org.junit.Test
9−
10−class PureCoreFfiTest {
11−
12− private fun createCore(): PureCoreFfi = PureCoreFfi()
13−
14− private fun PureCoreFfi.currentView(): ViewModel =
15− ViewModel.bincodeDeserialize(view())
16−
17− private fun PureCoreFfi.processEvent(event: Event): Requests {
18− val effects = update(event.bincodeSerialize())
19− return Requests.bincodeDeserialize(effects)
20− }
21−
22− private fun PureCoreFfi.resolveHttp(
23− requestId: UInt,
24− status: Int = 200,
25− body: String = "{}",
26− ): Requests {
27− val response = HttpResponse(
28− status = status.toUShort(),
29− headers = emptyList(),
30− body = com.novi.serde.Bytes(body.toByteArray()),
31− )
32− val result = HttpResult.Ok(response)
33− val effects = resolve(requestId, result.bincodeSerialize())
34− return Requests.bincodeDeserialize(effects)
35− }
36−
37− @Test
38− fun `start event sets default server URL`() {
39− val core = createCore()
40− core.processEvent(Event.Start)
41− val view = core.currentView()
42− assertEquals("http://localhost:4096", view.serverUrl)
43− assertEquals(Screen.CONNECT, view.screen)
44− }
45−
46− @Test
47− fun `server URL change updates view`() {
48− val core = createCore()
49− core.processEvent(Event.Start)
50− core.processEvent(Event.ServerUrlChanged("http://10.0.0.1:4096"))
51− val view = core.currentView()
52− assertEquals("http://10.0.0.1:4096", view.serverUrl)
53− }
54−
55− @Test
56− fun `connect emits HTTP effect`() {
57− val core = createCore()
58− core.processEvent(Event.Start)
59− val requests = core.processEvent(Event.Connect)
60− assertEquals(1, requests.value.size)
61− assertTrue(requests.value[0].effect is Effect.Http)
62− val view = core.currentView()
63− assertTrue(view.loading)
64− }
65−
66− @Test
67− fun `successful connect transitions to sessions screen`() {
68− val core = createCore()
69− core.processEvent(Event.Start)
70− val requests = core.processEvent(Event.Connect)
71− val httpReq = requests.value[0]
72− assertEquals(Effect.Http::class, httpReq.effect::class)
73−
74− // Simulate health check success — triggers LoadSessions
75− val followUpRequests = core.resolveHttp(httpReq.id, body = """{"healthy":true}""")
76− assertEquals(1, followUpRequests.value.size)
77−
78− // Simulate sessions loaded
79− val sessionsBody = """[{"id":"s1","title":"My Session"}]"""
80− val finalRequests = core.resolveHttp(followUpRequests.value[0].id, body = sessionsBody)
81− assertEquals(0, finalRequests.value.size)
82−
83− val view = core.currentView()
84− assertTrue(view.connected)
85− assertEquals(Screen.SESSIONS, view.screen)
86− assertEquals(1, view.sessions.size)
87− assertEquals("s1", view.sessions[0].id)
88− assertEquals("My Session", view.sessions[0].title)
89− }
90−
91− @Test
92− fun `failed connect shows error`() {
93− val core = createCore()
94− core.processEvent(Event.Start)
95− val requests = core.processEvent(Event.Connect)
96−
97− val errorResult = HttpResult.Err(HttpError.Io("Connection refused"))
98− val effects = core.resolve(requests.value[0].id, errorResult.bincodeSerialize())
99− Requests.bincodeDeserialize(effects)
100−
101− val view = core.currentView()
102− assertNotNull(view.error)
103− assertFalse(view.loading)
104− }
105−
106− @Test
107− fun `dismiss error clears error`() {
108− val core = createCore()
109− core.processEvent(Event.Start)
110− val requests = core.processEvent(Event.Connect)
111−
112− val errorResult = HttpResult.Err(HttpError.Io("Connection refused"))
113− core.resolve(requests.value[0].id, errorResult.bincodeSerialize())
114−
115− core.processEvent(Event.DismissError)
116− val view = core.currentView()
117− assertNull(view.error)
118− }
119−
120− @Test
121− fun `select session loads messages and transitions to chat`() {
122− val core = createCore()
123− core.processEvent(Event.Start)
124− val connectReqs = core.processEvent(Event.Connect)
125− core.resolveHttp(connectReqs.value[0].id, body = """{"healthy":true}""")
126− core.resolveHttp(0u, body = "[]") // won't find the id, but we can manually set sessions
127−
128− // Manually select a session
129− val selectReqs = core.processEvent(Event.SelectSession("session-123"))
130− assertTrue(selectReqs.value.isNotEmpty())
131−
132− val view = core.currentView()
133− assertEquals("session-123", view.currentSessionId)
134− assertEquals(Screen.CHAT, view.screen)
135− }
136−}
build.gradle.ktsdeleted−7
@@ -1,7 +0,0 @@
1−plugins {
2− alias(libs.plugins.android.application) apply false
3− alias(libs.plugins.kotlin.android) apply false
4− alias(libs.plugins.kotlin.compose) apply false
5− alias(libs.plugins.android.library) apply false
6− alias(libs.plugins.kotlin.serialization) apply false
7−}
flake.lockdeleted−61
@@ -1,61 +0,0 @@
1−{
2− "nodes": {
3− "flake-utils": {
4− "inputs": {
5− "systems": "systems"
6− },
7− "locked": {
8− "lastModified": 1731533236,
9− "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
10− "owner": "numtide",
11− "repo": "flake-utils",
12− "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
13− "type": "github"
14− },
15− "original": {
16− "owner": "numtide",
17− "repo": "flake-utils",
18− "type": "github"
19− }
20− },
21− "nixpkgs": {
22− "locked": {
23− "lastModified": 1782467914,
24− "narHash": "sha256-pGvFkM8N0xEkIIXDe5YYfbEAvHrk4IxBrjB/x8OomhE=",
25− "owner": "NixOS",
26− "repo": "nixpkgs",
27− "rev": "e73de5be04e0eff4190a1432b946d469c794e7b4",
28− "type": "github"
29− },
30− "original": {
31− "owner": "NixOS",
32− "ref": "nixos-unstable",
33− "repo": "nixpkgs",
34− "type": "github"
35− }
36− },
37− "root": {
38− "inputs": {
39− "flake-utils": "flake-utils",
40− "nixpkgs": "nixpkgs"
41− }
42− },
43− "systems": {
44− "locked": {
45− "lastModified": 1681028828,
46− "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
47− "owner": "nix-systems",
48− "repo": "default",
49− "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
50− "type": "github"
51− },
52− "original": {
53− "owner": "nix-systems",
54− "repo": "default",
55− "type": "github"
56− }
57− }
58− },
59− "root": "root",
60− "version": 7
61−}
flake.nixdeleted−71
@@ -1,71 +0,0 @@
1−{
2− description = "Dev environment for opencode-android (Kotlin / Jetpack Compose client for the opencode serve API)";
3−
4− inputs = {
5− nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6− flake-utils.url = "github:numtide/flake-utils";
7− };
8−
9− outputs = { self, nixpkgs, flake-utils }:
10− flake-utils.lib.eachDefaultSystem (system:
11− let
12− # Versions kept in sync with the Gradle project (compileSdk / build-tools).
13− platformVersion = "37";
14− buildToolsVersion = "37.0.0";
15−
16− pkgs = import nixpkgs {
17− inherit system;
18− config = {
19− allowUnfree = true; # Android SDK is unfree
20− android_sdk.accept_license = true;
21− };
22− };
23−
24− android = pkgs.androidenv.composeAndroidPackages {
25− platformVersions = [ platformVersion ];
26− buildToolsVersions = [ buildToolsVersion ];
27− cmdLineToolsVersion = "latest";
28−
29− # Flip these on if the host has /dev/kvm and you want an AVD.
30− includeEmulator = false;
31− includeSystemImages = false;
32− systemImageTypes = [ "google_apis" ];
33− abiVersions = [ "x86_64" ];
34−
35− includeNDK = false;
36− };
37−
38− sdk = android.androidsdk;
39− sdkRoot = "${sdk}/libexec/android-sdk";
40−
41− # Gradle's bundled aapt2 binary is dynamically linked against a loader
42− # path that does not exist on NixOS, so it segfaults. Point Gradle and
43− # Android Studio at the Nix-built aapt2 instead.
44− aapt2Path = "${sdkRoot}/build-tools/${buildToolsVersion}/aapt2";
45− in {
46− devShells.default = pkgs.mkShell {
47− buildInputs = [
48− pkgs.jdk17
49− pkgs.gradle
50− sdk
51− ];
52−
53− ANDROID_HOME = sdkRoot;
54− ANDROID_SDK_ROOT = sdkRoot;
55− JAVA_HOME = "${pkgs.jdk17}";
56−
57− # Override Gradle's bundled aapt2 (see note above).
58− GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${aapt2Path}";
59−
60− shellHook = ''
61− echo "opencode-android dev shell"
62− echo " JDK: $(java -version 2>&1 | head -1)"
63− echo " ANDROID_HOME: $ANDROID_HOME"
64− echo " aapt2: ${aapt2Path}"
65− echo ""
66− echo "Build: ./gradlew assembleDebug"
67− echo "Device: adb devices"
68− '';
69− };
70− });
71−}
generated/build.gradle.ktsdeleted−21
@@ -1,21 +0,0 @@
1−plugins {
2− kotlin("jvm") version "2.2.0"
3− kotlin("plugin.serialization") version "2.2.0"
4− `java-library`
5−}
6−
7−group = "soy.iko.opencode.core"
8−version = "1.0.0"
9−
10−repositories {
11− mavenCentral()
12−}
13−
14−dependencies {}
15−
16−tasks.withType<Jar> {
17− manifest {
18− attributes["Implementation-Title"] = "soy.iko.opencode.core"
19− attributes["Implementation-Version"] = "1.0.0"
20− }
21−}
generated/com/novi/bincode/BincodeDeserializer.ktdeleted−39
@@ -1,39 +0,0 @@
1−// Copyright (c) Zefchain Labs, Inc.
2−// SPDX-License-Identifier: MIT OR Apache-2.0
3−
4−package com.novi.bincode
5−
6−import com.novi.serde.BinaryDeserializer
7−import com.novi.serde.DeserializationError
8−import com.novi.serde.Slice
9−
10−class BincodeDeserializer(input: ByteArray) : BinaryDeserializer(input, Long.MAX_VALUE) {
11− @Throws(DeserializationError::class)
12− override fun deserialize_f32(): Float {
13− return Float.fromBits(getInt())
14− }
15−
16− @Throws(DeserializationError::class)
17− override fun deserialize_f64(): Double {
18− return Double.fromBits(getLong())
19− }
20−
21− @Throws(DeserializationError::class)
22− override fun deserialize_len(): Long {
23− val value = getLong()
24− if (value < 0 || value > Int.MAX_VALUE.toLong()) {
25− throw DeserializationError("Incorrect length value")
26− }
27− return value
28− }
29−
30− @Throws(DeserializationError::class)
31− override fun deserialize_variant_index(): Int {
32− return getInt()
33− }
34−
35− @Throws(DeserializationError::class)
36− override fun check_that_key_slices_are_increasing(key1: Slice, key2: Slice) {
37− // Not required by the format.
38− }
39−}
generated/com/novi/bincode/BincodeSerializer.ktdeleted−33
@@ -1,33 +0,0 @@
1−// Copyright (c) Zefchain Labs, Inc.
2−// SPDX-License-Identifier: MIT OR Apache-2.0
3−
4−package com.novi.bincode
5−
6−import com.novi.serde.BinarySerializer
7−import com.novi.serde.SerializationError
8−
9−class BincodeSerializer : BinarySerializer(Long.MAX_VALUE) {
10− @Throws(SerializationError::class)
11− override fun serialize_f32(value: Float) {
12− serialize_i32(value.toRawBits())
13− }
14−
15− @Throws(SerializationError::class)
16− override fun serialize_f64(value: Double) {
17− serialize_i64(value.toRawBits())
18− }
19−
20− @Throws(SerializationError::class)
21− override fun serialize_len(value: Long) {
22− serialize_u64(value.toULong())
23− }
24−
25− @Throws(SerializationError::class)
26− override fun serialize_variant_index(value: Int) {
27− serialize_u32(value.toUInt())
28− }
29−
30− override fun sort_map_entries(offsets: IntArray) {
31− // Not required by the format.
32− }
33−}
generated/com/novi/serde/BinaryDeserializer.ktdeleted−182
@@ -1,182 +0,0 @@
1−// Copyright (c) Zefchain Labs, Inc.
2−// SPDX-License-Identifier: MIT OR Apache-2.0
3−
4−package com.novi.serde
5−
6−abstract class BinaryDeserializer(
7− protected val input: ByteArray,
8− maxContainerDepth: Long
9−) : Deserializer {
10− private var position: Int = 0
11− private var containerDepthBudget: Long = maxContainerDepth
12−
13− @Throws(DeserializationError::class)
14− override fun increase_container_depth() {
15− if (containerDepthBudget == 0L) {
16− throw DeserializationError("Exceeded maximum container depth")
17− }
18− containerDepthBudget -= 1
19− }
20−
21− override fun decrease_container_depth() {
22− containerDepthBudget += 1
23− }
24−
25− @Throws(DeserializationError::class)
26− override fun deserialize_str(): String {
27− val len = deserialize_len()
28− if (len < 0 || len > Int.MAX_VALUE.toLong()) {
29− throw DeserializationError("Incorrect length value for Kotlin string")
30− }
31− val content = readBytes(len.toInt())
32− return try {
33− content.decodeToString(throwOnInvalidSequence = true)
34− } catch (e: Throwable) {
35− throw DeserializationError("Incorrect UTF8 string")
36− }
37− }
38−
39− @Throws(DeserializationError::class)
40− override fun deserialize_bytes(): Bytes {
41− val len = deserialize_len()
42− if (len < 0 || len > Int.MAX_VALUE.toLong()) {
43− throw DeserializationError("Incorrect length value for Kotlin array")
44− }
45− return Bytes(readBytes(len.toInt()))
46− }
47−
48− @Throws(DeserializationError::class)
49− override fun deserialize_bool(): Boolean {
50− val value = getByte()
51− return when (value.toInt()) {
52− 0 -> false
53− 1 -> true
54− else -> throw DeserializationError("Incorrect boolean value")
55− }
56− }
57−
58− @Throws(DeserializationError::class)
59− override fun deserialize_unit(): Unit {
60− return Unit
61− }
62−
63− @Throws(DeserializationError::class)
64− override fun deserialize_char(): Char {
65− throw DeserializationError("Not implemented: deserialize_char")
66− }
67−
68− @Throws(DeserializationError::class)
69− override fun deserialize_u8(): UByte {
70− return getByte().toUByte()
71− }
72−
73− @Throws(DeserializationError::class)
74− override fun deserialize_u16(): UShort {
75− val b0 = getByte().toInt() and 0xff
76− val b1 = getByte().toInt() and 0xff
77− return (b0 or (b1 shl 8)).toUShort()
78− }
79−
80− @Throws(DeserializationError::class)
81− override fun deserialize_u32(): UInt {
82− val b0 = getByte().toInt() and 0xff
83− val b1 = getByte().toInt() and 0xff
84− val b2 = getByte().toInt() and 0xff
85− val b3 = getByte().toInt() and 0xff
86− return (b0 or (b1 shl 8) or (b2 shl 16) or (b3 shl 24)).toUInt()
87− }
88−
89− @Throws(DeserializationError::class)
90− override fun deserialize_u64(): ULong {
91− var value = 0uL
92− for (shift in 0 until 64 step 8) {
93− val byteValue = getByte().toULong() and 0xffuL
94− value = value or (byteValue shl shift)
95− }
96− return value
97− }
98−
99− @Throws(DeserializationError::class)
100− override fun deserialize_u128(): UInt128 {
101− val low = deserialize_u64()
102− val high = deserialize_u64()
103− return UInt128(high = high, low = low)
104− }
105−
106− @Throws(DeserializationError::class)
107− override fun deserialize_i8(): Byte {
108− return getByte()
109− }
110−
111− @Throws(DeserializationError::class)
112− override fun deserialize_i16(): Short {
113− return deserialize_u16().toShort()
114− }
115−
116− @Throws(DeserializationError::class)
117− override fun deserialize_i32(): Int {
118− return deserialize_u32().toInt()
119− }
120−
121− @Throws(DeserializationError::class)
122− override fun deserialize_i64(): Long {
123− return deserialize_u64().toLong()
124− }
125−
126− @Throws(DeserializationError::class)
127− override fun deserialize_i128(): Int128 {
128− val low = deserialize_u64()
129− val high = deserialize_i64()
130− return Int128(high = high, low = low)
131− }
132−
133− @Throws(DeserializationError::class)
134− override fun deserialize_option_tag(): Boolean {
135− return deserialize_bool()
136− }
137−
138− override fun get_buffer_offset(): Int {
139− return position
140− }
141−
142− protected fun getInt(): Int {
143− val b0 = getByte().toInt() and 0xff
144− val b1 = getByte().toInt() and 0xff
145− val b2 = getByte().toInt() and 0xff
146− val b3 = getByte().toInt() and 0xff
147− return b0 or (b1 shl 8) or (b2 shl 16) or (b3 shl 24)
148− }
149−
150− protected fun getLong(): Long {
151− var value = 0L
152− for (shift in 0 until 64 step 8) {
153− val byteValue = getByte().toLong() and 0xffL
154− value = value or (byteValue shl shift)
155− }
156− return value
157− }
158−
159− protected fun getByte(): Byte {
160− requireAvailable(1)
161− val value = input[position]
162− position += 1
163− return value
164− }
165−
166− private fun readBytes(count: Int): ByteArray {
167− requireAvailable(count)
168− val slice = input.copyOfRange(position, position + count)
169− position += count
170− return slice
171− }
172−
173− private fun requireAvailable(count: Int) {
174− if (position + count > input.size) {
175− throw DeserializationError(INPUT_NOT_LARGE_ENOUGH)
176− }
177− }
178−
179− companion object {
180− private const val INPUT_NOT_LARGE_ENOUGH = "Input is not large enough"
181− }
182−}
generated/com/novi/serde/BinarySerializer.ktdeleted−122
@@ -1,122 +0,0 @@
1−// Copyright (c) Zefchain Labs, Inc.
2−// SPDX-License-Identifier: MIT OR Apache-2.0
3−
4−package com.novi.serde
5−
6−abstract class BinarySerializer(maxContainerDepth: Long) : Serializer {
7− protected val output: SerdeByteArrayOutput = SerdeByteArrayOutput()
8− private var containerDepthBudget: Long = maxContainerDepth
9−
10− @Throws(SerializationError::class)
11− override fun increase_container_depth() {
12− if (containerDepthBudget == 0L) {
13− throw SerializationError("Exceeded maximum container depth")
14− }
15− containerDepthBudget -= 1
16− }
17−
18− override fun decrease_container_depth() {
19− containerDepthBudget += 1
20− }
21−
22− @Throws(SerializationError::class)
23− override fun serialize_str(value: String) {
24− serialize_bytes(Bytes(value.encodeToByteArray()))
25− }
26−
27− @Throws(SerializationError::class)
28− override fun serialize_bytes(value: Bytes) {
29− serialize_len(value.content.size.toLong())
30− output.writeBytes(value.content, 0, value.content.size)
31− }
32−
33− @Throws(SerializationError::class)
34− override fun serialize_bool(value: Boolean) {
35− output.writeByte(if (value) 1.toByte() else 0.toByte())
36− }
37−
38− @Throws(SerializationError::class)
39− override fun serialize_unit(value: Unit) {
40− // Nothing to serialize.
41− }
42−
43− @Throws(SerializationError::class)
44− override fun serialize_char(value: Char) {
45− throw SerializationError("Not implemented: serialize_char")
46− }
47−
48− @Throws(SerializationError::class)
49− override fun serialize_u8(value: UByte) {
50− output.writeByte(value.toByte())
51− }
52−
53− @Throws(SerializationError::class)
54− override fun serialize_u16(value: UShort) {
55− val v = value.toInt()
56− output.writeByte((v and 0xff).toByte())
57− output.writeByte(((v ushr 8) and 0xff).toByte())
58− }
59−
60− @Throws(SerializationError::class)
61− override fun serialize_u32(value: UInt) {
62− val v = value.toInt()
63− output.writeByte((v and 0xff).toByte())
64− output.writeByte(((v ushr 8) and 0xff).toByte())
65− output.writeByte(((v ushr 16) and 0xff).toByte())
66− output.writeByte(((v ushr 24) and 0xff).toByte())
67− }
68−
69− @Throws(SerializationError::class)
70− override fun serialize_u64(value: ULong) {
71− var v = value
72− for (i in 0 until 8) {
73− output.writeByte((v and 0xffuL).toByte())
74− v = v shr 8
75− }
76− }
77−
78− @Throws(SerializationError::class)
79− override fun serialize_u128(value: UInt128) {
80− serialize_u64(value.low)
81− serialize_u64(value.high)
82− }
83−
84− @Throws(SerializationError::class)
85− override fun serialize_i8(value: Byte) {
86− serialize_u8(value.toUByte())
87− }
88−
89− @Throws(SerializationError::class)
90− override fun serialize_i16(value: Short) {
91− serialize_u16(value.toUShort())
92− }
93−
94− @Throws(SerializationError::class)
95− override fun serialize_i32(value: Int) {
96− serialize_u32(value.toUInt())
97− }
98−
99− @Throws(SerializationError::class)
100− override fun serialize_i64(value: Long) {
101− serialize_u64(value.toULong())
102− }
103−
104− @Throws(SerializationError::class)
105− override fun serialize_i128(value: Int128) {
106− serialize_u64(value.low)
107− serialize_i64(value.high)
108− }
109−
110− @Throws(SerializationError::class)
111− override fun serialize_option_tag(value: Boolean) {
112− output.writeByte(if (value) 1.toByte() else 0.toByte())
113− }
114−
115− override fun get_buffer_offset(): Int {
116− return output.size()
117− }
118−
119− override fun get_bytes(): ByteArray {
120− return output.toByteArray()
121− }
122−}
generated/com/novi/serde/Bytes.ktdeleted−39
@@ -1,39 +0,0 @@
1−// Copyright (c) Zefchain Labs, Inc.
2−// SPDX-License-Identifier: MIT OR Apache-2.0
3−
4−package com.novi.serde
5−
6−/**
7− * Inline value class wrapper around ByteArray.
8− *
9− * Provides proper value semantics for `equals` and `hashCode` using
10− * structural equality (contentEquals/contentHashCode) instead of
11− * referential equality.
12− *
13− * As a value class, this wrapper has zero runtime overhead - it's
14− * inlined at compile time and compiles down to ByteArray at runtime.
15− */
16−@JvmInline
17−value class Bytes(val content: ByteArray) {
18− override fun toString(): String = content.contentToString()
19−
20− companion object {
21− fun empty(): Bytes = Bytes(ByteArray(0))
22−
23− fun valueOf(content: ByteArray): Bytes = Bytes(content)
24− }
25−}
26−
27−/**
28− * Extension function for structural equality comparison.
29− * Required because value classes don't support overriding equals.
30− */
31−fun Bytes.contentEquals(other: Bytes): Boolean =
32− this.content.contentEquals(other.content)
33−
34−/**
35− * Extension function for structural hash code.
36− * Required because value classes don't support overriding hashCode.
37− */
38−fun Bytes.contentHashCode(): Int =
39− content.contentHashCode()
generated/com/novi/serde/DeserializationError.ktdeleted−6
@@ -1,6 +0,0 @@
1−// Copyright (c) Zefchain Labs, Inc.
2−// SPDX-License-Identifier: MIT OR Apache-2.0
3−
4−package com.novi.serde
5−
6−class DeserializationError(message: String) : Exception(message)
generated/com/novi/serde/Deserializer.ktdeleted−76
@@ -1,76 +0,0 @@
1−// Copyright (c) Zefchain Labs, Inc.
2−// SPDX-License-Identifier: MIT OR Apache-2.0
3−
4−package com.novi.serde
5−
6−interface Deserializer {
7− @Throws(DeserializationError::class)
8− fun deserialize_str(): String
9−
10− @Throws(DeserializationError::class)
11− fun deserialize_bytes(): Bytes
12−
13− @Throws(DeserializationError::class)
14− fun deserialize_bool(): Boolean
15−
16− @Throws(DeserializationError::class)
17− fun deserialize_unit(): Unit
18−
19− @Throws(DeserializationError::class)
20− fun deserialize_char(): Char
21−
22− @Throws(DeserializationError::class)
23− fun deserialize_f32(): Float
24−
25− @Throws(DeserializationError::class)
26− fun deserialize_f64(): Double
27−
28− @Throws(DeserializationError::class)
29− fun deserialize_u8(): UByte
30−
31− @Throws(DeserializationError::class)
32− fun deserialize_u16(): UShort
33−
34− @Throws(DeserializationError::class)
35− fun deserialize_u32(): UInt
36−
37− @Throws(DeserializationError::class)
38− fun deserialize_u64(): ULong
39−
40− @Throws(DeserializationError::class)
41− fun deserialize_u128(): UInt128
42−
43− @Throws(DeserializationError::class)
44− fun deserialize_i8(): Byte
45−
46− @Throws(DeserializationError::class)
47− fun deserialize_i16(): Short
48−
49− @Throws(DeserializationError::class)
50− fun deserialize_i32(): Int
51−
52− @Throws(DeserializationError::class)
53− fun deserialize_i64(): Long
54−
55− @Throws(DeserializationError::class)
56− fun deserialize_i128(): Int128
57−
58− @Throws(DeserializationError::class)
59− fun deserialize_len(): Long
60−
61− @Throws(DeserializationError::class)
62− fun deserialize_variant_index(): Int
63−
64− @Throws(DeserializationError::class)
65− fun deserialize_option_tag(): Boolean
66−
67− @Throws(DeserializationError::class)
68− fun increase_container_depth()
69−
70− fun decrease_container_depth()
71−
72− fun get_buffer_offset(): Int
73−
74− @Throws(DeserializationError::class)
75− fun check_that_key_slices_are_increasing(key1: Slice, key2: Slice)
76−}
generated/com/novi/serde/Int128.ktdeleted−6
@@ -1,6 +0,0 @@
1−// Copyright (c) Zefchain Labs, Inc.
2−// SPDX-License-Identifier: MIT OR Apache-2.0
3−
4−package com.novi.serde
5−
6−data class Int128(val high: Long, val low: ULong)
generated/com/novi/serde/SerdeByteArrayOutput.ktdeleted−48
@@ -1,48 +0,0 @@
1−// Copyright (c) Zefchain Labs, Inc.
2−// SPDX-License-Identifier: MIT OR Apache-2.0
3−
4−package com.novi.serde
5−
6−class SerdeByteArrayOutput(initialCapacity: Int = 32) {
7− private var buffer: ByteArray = ByteArray(initialCapacity)
8− private var length: Int = 0
9−
10− fun writeByte(value: Byte) {
11− ensureCapacity(1)
12− buffer[length] = value
13− length += 1
14− }
15−
16− fun writeBytes(value: ByteArray, offset: Int, count: Int) {
17− if (count == 0) {
18− return
19− }
20− ensureCapacity(count)
21− value.copyInto(buffer, destinationOffset = length, startIndex = offset, endIndex = offset + count)
22− length += count
23− }
24−
25− fun size(): Int {
26− return length
27− }
28−
29− fun toByteArray(): ByteArray {
30− return buffer.copyOf(length)
31− }
32−
33− fun getBuffer(): ByteArray {
34− return buffer
35− }
36−
37− private fun ensureCapacity(extra: Int) {
38− val required = length + extra
39− if (required <= buffer.size) {
40− return
41− }
42− var newSize = buffer.size
43− while (newSize < required) {
44− newSize = newSize * 2
45− }
46− buffer = buffer.copyOf(newSize)
47− }
48−}
generated/com/novi/serde/SerializationError.ktdeleted−6
@@ -1,6 +0,0 @@
1−// Copyright (c) Zefchain Labs, Inc.
2−// SPDX-License-Identifier: MIT OR Apache-2.0
3−
4−package com.novi.serde
5−
6−class SerializationError(message: String) : Exception(message)
generated/com/novi/serde/Serializer.ktdeleted−77
@@ -1,77 +0,0 @@
1−// Copyright (c) Zefchain Labs, Inc.
2−// SPDX-License-Identifier: MIT OR Apache-2.0
3−
4−package com.novi.serde
5−
6−interface Serializer {
7− @Throws(SerializationError::class)
8− fun serialize_str(value: String)
9−
10− @Throws(SerializationError::class)
11− fun serialize_bytes(value: Bytes)
12−
13− @Throws(SerializationError::class)
14− fun serialize_bool(value: Boolean)
15−
16− @Throws(SerializationError::class)
17− fun serialize_unit(value: Unit)
18−
19− @Throws(SerializationError::class)
20− fun serialize_char(value: Char)
21−
22− @Throws(SerializationError::class)
23− fun serialize_f32(value: Float)
24−
25− @Throws(SerializationError::class)
26− fun serialize_f64(value: Double)
27−
28− @Throws(SerializationError::class)
29− fun serialize_u8(value: UByte)
30−
31− @Throws(SerializationError::class)
32− fun serialize_u16(value: UShort)
33−
34− @Throws(SerializationError::class)
35− fun serialize_u32(value: UInt)
36−
37− @Throws(SerializationError::class)
38− fun serialize_u64(value: ULong)
39−
40− @Throws(SerializationError::class)
41− fun serialize_u128(value: UInt128)
42−
43− @Throws(SerializationError::class)
44− fun serialize_i8(value: Byte)
45−
46− @Throws(SerializationError::class)
47− fun serialize_i16(value: Short)
48−
49− @Throws(SerializationError::class)
50− fun serialize_i32(value: Int)
51−
52− @Throws(SerializationError::class)
53− fun serialize_i64(value: Long)
54−
55− @Throws(SerializationError::class)
56− fun serialize_i128(value: Int128)
57−
58− @Throws(SerializationError::class)
59− fun serialize_len(value: Long)
60−
61− @Throws(SerializationError::class)
62− fun serialize_variant_index(value: Int)
63−
64− @Throws(SerializationError::class)
65− fun serialize_option_tag(value: Boolean)
66−
67− @Throws(SerializationError::class)
68− fun increase_container_depth()
69−
70− fun decrease_container_depth()
71−
72− fun get_buffer_offset(): Int
73−
74− fun sort_map_entries(offsets: IntArray)
75−
76− fun get_bytes(): ByteArray
77−}
generated/com/novi/serde/Slice.ktdeleted−36
@@ -1,36 +0,0 @@
1−// Copyright (c) Zefchain Labs, Inc.
2−// SPDX-License-Identifier: MIT OR Apache-2.0
3−
4−package com.novi.serde
5−
6−data class Slice(val start: Int, val end: Int) {
7− companion object {
8− fun compare_bytes(content: ByteArray, slice1: Slice, slice2: Slice): Int {
9− val start1 = slice1.start
10− val end1 = slice1.end
11− val start2 = slice2.start
12− val end2 = slice2.end
13− var i = 0
14− while (i < end1 - start1) {
15− val index1 = start1 + i
16− val index2 = start2 + i
17− val byte1 = content[index1].toInt() and 0xff
18− if (index2 >= end2) {
19− return 1
20− }
21− val byte2 = content[index2].toInt() and 0xff
22− if (byte1 > byte2) {
23− return 1
24− }
25− if (byte1 < byte2) {
26− return -1
27− }
28− i += 1
29− }
30− if (end2 - start2 > end1 - start1) {
31− return -1
32− }
33− return 0
34− }
35− }
36−}
generated/com/novi/serde/Tuple4.ktdeleted−6
@@ -1,6 +0,0 @@
1−// Copyright (c) Zefchain Labs, Inc.
2−// SPDX-License-Identifier: MIT OR Apache-2.0
3−
4−package com.novi.serde
5−
6−data class Tuple4<T0, T1, T2, T3>(val field0: T0, val field1: T1, val field2: T2, val field3: T3)
generated/com/novi/serde/Tuple5.ktdeleted−12
@@ -1,12 +0,0 @@
1−// Copyright (c) Zefchain Labs, Inc.
2−// SPDX-License-Identifier: MIT OR Apache-2.0
3−
4−package com.novi.serde
5−
6−data class Tuple5<T0, T1, T2, T3, T4>(
7− val field0: T0,
8− val field1: T1,
9− val field2: T2,
10− val field3: T3,
11− val field4: T4
12−)
generated/com/novi/serde/Tuple6.ktdeleted−13
@@ -1,13 +0,0 @@
1−// Copyright (c) Zefchain Labs, Inc.
2−// SPDX-License-Identifier: MIT OR Apache-2.0
3−
4−package com.novi.serde
5−
6−data class Tuple6<T0, T1, T2, T3, T4, T5>(
7− val field0: T0,
8− val field1: T1,
9− val field2: T2,
10− val field3: T3,
11− val field4: T4,
12− val field5: T5
13−)
generated/com/novi/serde/UInt128.ktdeleted−6
@@ -1,6 +0,0 @@
1−// Copyright (c) Zefchain Labs, Inc.
2−// SPDX-License-Identifier: MIT OR Apache-2.0
3−
4−package com.novi.serde
5−
6−data class UInt128(val high: ULong, val low: ULong)
generated/soy/iko/opencode/core/Core.ktdeleted−1110
@@ -1,1110 +0,0 @@
1−package soy.iko.opencode.core
2−
3−import com.novi.bincode.BincodeDeserializer
4−import com.novi.bincode.BincodeSerializer
5−import com.novi.serde.Bytes
6−import com.novi.serde.DeserializationError
7−import com.novi.serde.Deserializer
8−import com.novi.serde.Serializer
9−
10−fun <T> List<T>.serialize(
11− serializer: Serializer,
12− serializeElement: Serializer.(T) -> Unit,
13−) {
14− serializer.serialize_len(size.toLong())
15− forEach { element ->
16− serializer.serializeElement(element)
17− }
18−}
19−
20−fun <T> Deserializer.deserializeListOf(deserializeElement: (Deserializer) -> T): List<T> {
21− val length = deserialize_len()
22− val list = mutableListOf<T>()
23− repeat(length.toInt()) {
24− list.add(deserializeElement(this))
25− }
26− return list
27−}
28−
29−fun <T> T?.serializeOptionOf(
30− serializer: Serializer,
31− serializeElement: Serializer.(T) -> Unit,
32−) {
33− if (this != null) {
34− serializer.serialize_option_tag(true)
35− serializer.serializeElement(this)
36− } else {
37− serializer.serialize_option_tag(false)
38− }
39−}
40−
41−fun <T> Deserializer.deserializeOptionOf(deserializeElement: (Deserializer) -> T): T? {
42− val tag = deserialize_option_tag()
43− return if (tag) {
44− deserializeElement(this)
45− } else {
46− null
47− }
48−}
49−
50−sealed interface Effect {
51− fun serialize(serializer: Serializer)
52−
53− fun bincodeSerialize(): ByteArray {
54− val serializer = BincodeSerializer()
55− serialize(serializer)
56− return serializer.get_bytes()
57− }
58−
59− data class Render(
60− val value: soy.iko.opencode.core.RenderOperation,
61− ) : Effect {
62− override fun serialize(serializer: Serializer) {
63− serializer.increase_container_depth()
64− serializer.serialize_variant_index(0)
65− value.serialize(serializer)
66− serializer.decrease_container_depth()
67− }
68−
69− companion object {
70− fun deserialize(deserializer: Deserializer): Render {
71− deserializer.increase_container_depth()
72− val value = soy.iko.opencode.core.RenderOperation.deserialize(deserializer)
73− deserializer.decrease_container_depth()
74− return Render(value)
75− }
76− }
77− }
78−
79− data class Http(
80− val value: soy.iko.opencode.core.HttpRequest,
81− ) : Effect {
82− override fun serialize(serializer: Serializer) {
83− serializer.increase_container_depth()
84− serializer.serialize_variant_index(1)
85− value.serialize(serializer)
86− serializer.decrease_container_depth()
87− }
88−
89− companion object {
90− fun deserialize(deserializer: Deserializer): Http {
91− deserializer.increase_container_depth()
92− val value = soy.iko.opencode.core.HttpRequest.deserialize(deserializer)
93− deserializer.decrease_container_depth()
94− return Http(value)
95− }
96− }
97− }
98−
99− companion object {
100− @Throws(DeserializationError::class)
101− fun deserialize(deserializer: Deserializer): Effect {
102− val index = deserializer.deserialize_variant_index()
103− return when (index) {
104− 0 -> Render.deserialize(deserializer)
105− 1 -> Http.deserialize(deserializer)
106− else -> throw DeserializationError("Unknown variant index for Effect: $index")
107− }
108− }
109−
110− @Throws(DeserializationError::class)
111− fun bincodeDeserialize(input: ByteArray?): Effect {
112− if (input == null) {
113− throw DeserializationError("Cannot deserialize null array")
114− }
115− val deserializer = BincodeDeserializer(input)
116− val value = deserialize(deserializer)
117− if (deserializer.get_buffer_offset() < input.size) {
118− throw DeserializationError("Some input bytes were not read")
119− }
120− return value
121− }
122− }
123−}
124−
125−sealed interface Event {
126− fun serialize(serializer: Serializer)
127−
128− fun bincodeSerialize(): ByteArray {
129− val serializer = BincodeSerializer()
130− serialize(serializer)
131− return serializer.get_bytes()
132− }
133−
134− data object Start: Event {
135− override fun serialize(serializer: Serializer) {
136− serializer.increase_container_depth()
137− serializer.serialize_variant_index(0)
138− serializer.decrease_container_depth()
139− }
140−
141− fun deserialize(deserializer: Deserializer): Start {
142− return Start
143− }
144− }
145−
146− data class ServerUrlChanged(
147− val value: String,
148− ) : Event {
149− override fun serialize(serializer: Serializer) {
150− serializer.increase_container_depth()
151− serializer.serialize_variant_index(1)
152− serializer.serialize_str(value)
153− serializer.decrease_container_depth()
154− }
155−
156− companion object {
157− fun deserialize(deserializer: Deserializer): ServerUrlChanged {
158− deserializer.increase_container_depth()
159− val value = deserializer.deserialize_str()
160− deserializer.decrease_container_depth()
161− return ServerUrlChanged(value)
162− }
163− }
164− }
165−
166− data object Connect: Event {
167− override fun serialize(serializer: Serializer) {
168− serializer.increase_container_depth()
169− serializer.serialize_variant_index(2)
170− serializer.decrease_container_depth()
171− }
172−
173− fun deserialize(deserializer: Deserializer): Connect {
174− return Connect
175− }
176− }
177−
178− data object LoadSessions: Event {
179− override fun serialize(serializer: Serializer) {
180− serializer.increase_container_depth()
181− serializer.serialize_variant_index(3)
182− serializer.decrease_container_depth()
183− }
184−
185− fun deserialize(deserializer: Deserializer): LoadSessions {
186− return LoadSessions
187− }
188− }
189−
190− data class SelectSession(
191− val value: String,
192− ) : Event {
193− override fun serialize(serializer: Serializer) {
194− serializer.increase_container_depth()
195− serializer.serialize_variant_index(4)
196− serializer.serialize_str(value)
197− serializer.decrease_container_depth()
198− }
199−
200− companion object {
201− fun deserialize(deserializer: Deserializer): SelectSession {
202− deserializer.increase_container_depth()
203− val value = deserializer.deserialize_str()
204− deserializer.decrease_container_depth()
205− return SelectSession(value)
206− }
207− }
208− }
209−
210− data object CreateSession: Event {
211− override fun serialize(serializer: Serializer) {
212− serializer.increase_container_depth()
213− serializer.serialize_variant_index(5)
214− serializer.decrease_container_depth()
215− }
216−
217− fun deserialize(deserializer: Deserializer): CreateSession {
218− return CreateSession
219− }
220− }
221−
222− data object SessionCreated: Event {
223− override fun serialize(serializer: Serializer) {
224− serializer.increase_container_depth()
225− serializer.serialize_variant_index(6)
226− serializer.decrease_container_depth()
227− }
228−
229− fun deserialize(deserializer: Deserializer): SessionCreated {
230− return SessionCreated
231− }
232− }
233−
234− data class LoadMessages(
235− val value: String,
236− ) : Event {
237− override fun serialize(serializer: Serializer) {
238− serializer.increase_container_depth()
239− serializer.serialize_variant_index(7)
240− serializer.serialize_str(value)
241− serializer.decrease_container_depth()
242− }
243−
244− companion object {
245− fun deserialize(deserializer: Deserializer): LoadMessages {
246− deserializer.increase_container_depth()
247− val value = deserializer.deserialize_str()
248− deserializer.decrease_container_depth()
249− return LoadMessages(value)
250− }
251− }
252− }
253−
254− data object MessagesLoaded: Event {
255− override fun serialize(serializer: Serializer) {
256− serializer.increase_container_depth()
257− serializer.serialize_variant_index(8)
258− serializer.decrease_container_depth()
259− }
260−
261− fun deserialize(deserializer: Deserializer): MessagesLoaded {
262− return MessagesLoaded
263− }
264− }
265−
266− data class SendMessage(
267− val value: String,
268− ) : Event {
269− override fun serialize(serializer: Serializer) {
270− serializer.increase_container_depth()
271− serializer.serialize_variant_index(9)
272− serializer.serialize_str(value)
273− serializer.decrease_container_depth()
274− }
275−
276− companion object {
277− fun deserialize(deserializer: Deserializer): SendMessage {
278− deserializer.increase_container_depth()
279− val value = deserializer.deserialize_str()
280− deserializer.decrease_container_depth()
281− return SendMessage(value)
282− }
283− }
284− }
285−
286− data object MessageSent: Event {
287− override fun serialize(serializer: Serializer) {
288− serializer.increase_container_depth()
289− serializer.serialize_variant_index(10)
290− serializer.decrease_container_depth()
291− }
292−
293− fun deserialize(deserializer: Deserializer): MessageSent {
294− return MessageSent
295− }
296− }
297−
298− data class EventReceived(
299− val value: String,
300− ) : Event {
301− override fun serialize(serializer: Serializer) {
302− serializer.increase_container_depth()
303− serializer.serialize_variant_index(11)
304− serializer.serialize_str(value)
305− serializer.decrease_container_depth()
306− }
307−
308− companion object {
309− fun deserialize(deserializer: Deserializer): EventReceived {
310− deserializer.increase_container_depth()
311− val value = deserializer.deserialize_str()
312− deserializer.decrease_container_depth()
313− return EventReceived(value)
314− }
315− }
316− }
317−
318− data class NavigateToChat(
319− val value: String,
320− ) : Event {
321− override fun serialize(serializer: Serializer) {
322− serializer.increase_container_depth()
323− serializer.serialize_variant_index(12)
324− serializer.serialize_str(value)
325− serializer.decrease_container_depth()
326− }
327−
328− companion object {
329− fun deserialize(deserializer: Deserializer): NavigateToChat {
330− deserializer.increase_container_depth()
331− val value = deserializer.deserialize_str()
332− deserializer.decrease_container_depth()
333− return NavigateToChat(value)
334− }
335− }
336− }
337−
338− data object NavigateToSessions: Event {
339− override fun serialize(serializer: Serializer) {
340− serializer.increase_container_depth()
341− serializer.serialize_variant_index(13)
342− serializer.decrease_container_depth()
343− }
344−
345− fun deserialize(deserializer: Deserializer): NavigateToSessions {
346− return NavigateToSessions
347− }
348− }
349−
350− data object DismissError: Event {
351− override fun serialize(serializer: Serializer) {
352− serializer.increase_container_depth()
353− serializer.serialize_variant_index(14)
354− serializer.decrease_container_depth()
355− }
356−
357− fun deserialize(deserializer: Deserializer): DismissError {
358− return DismissError
359− }
360− }
361−
362− companion object {
363− @Throws(DeserializationError::class)
364− fun deserialize(deserializer: Deserializer): Event {
365− val index = deserializer.deserialize_variant_index()
366− return when (index) {
367− 0 -> Start.deserialize(deserializer)
368− 1 -> ServerUrlChanged.deserialize(deserializer)
369− 2 -> Connect.deserialize(deserializer)
370− 3 -> LoadSessions.deserialize(deserializer)
371− 4 -> SelectSession.deserialize(deserializer)
372− 5 -> CreateSession.deserialize(deserializer)
373− 6 -> SessionCreated.deserialize(deserializer)
374− 7 -> LoadMessages.deserialize(deserializer)
375− 8 -> MessagesLoaded.deserialize(deserializer)
376− 9 -> SendMessage.deserialize(deserializer)
377− 10 -> MessageSent.deserialize(deserializer)
378− 11 -> EventReceived.deserialize(deserializer)
379− 12 -> NavigateToChat.deserialize(deserializer)
380− 13 -> NavigateToSessions.deserialize(deserializer)
381− 14 -> DismissError.deserialize(deserializer)
382− else -> throw DeserializationError("Unknown variant index for Event: $index")
383− }
384− }
385−
386− @Throws(DeserializationError::class)
387− fun bincodeDeserialize(input: ByteArray?): Event {
388− if (input == null) {
389− throw DeserializationError("Cannot deserialize null array")
390− }
391− val deserializer = BincodeDeserializer(input)
392− val value = deserialize(deserializer)
393− if (deserializer.get_buffer_offset() < input.size) {
394− throw DeserializationError("Some input bytes were not read")
395− }
396− return value
397− }
398− }
399−}
400−
401−/// An error produced when an HTTP request fails.
402−///
403−/// Variants fall into two groups:
404−///
405−/// **Transport errors** — generated by the shell when it cannot complete the HTTP
406−/// exchange. These cross the FFI boundary and are serialized in the protocol:
407−/// [`Url`](HttpError::Url), [`Io`](HttpError::Io), [`Timeout`](HttpError::Timeout).
408−///
409−/// **Processing errors** — generated on the Rust side after a response arrives.
410−/// These are never serialized or visible to shells:
411−///
412−/// - [`Http`](HttpError::Http) — produced by `Response::new()` when the server returns
413−/// a 4xx or 5xx status. At the *protocol* level these arrive as
414−/// [`HttpResult::Ok`](crate::protocol::HttpResult::Ok); `Response::new()` converts
415−/// them here, so app code using `crux_http::Result<Response<T>>` will see them as
416−/// `Err(HttpError::Http { code, .. })`.
417−/// - [`Json`](HttpError::Json) — produced when response body deserialisation fails.
418−sealed interface HttpError {
419− fun serialize(serializer: Serializer)
420−
421− fun bincodeSerialize(): ByteArray {
422− val serializer = BincodeSerializer()
423− serialize(serializer)
424− return serializer.get_bytes()
425− }
426−
427− /// The request URL could not be parsed.
428− data class Url(
429− val value: String,
430− ) : HttpError {
431− override fun serialize(serializer: Serializer) {
432− serializer.increase_container_depth()
433− serializer.serialize_variant_index(0)
434− serializer.serialize_str(value)
435− serializer.decrease_container_depth()
436− }
437−
438− companion object {
439− fun deserialize(deserializer: Deserializer): Url {
440− deserializer.increase_container_depth()
441− val value = deserializer.deserialize_str()
442− deserializer.decrease_container_depth()
443− return Url(value)
444− }
445− }
446− }
447−
448− /// An IO error prevented the request from completing.
449− data class Io(
450− val value: String,
451− ) : HttpError {
452− override fun serialize(serializer: Serializer) {
453− serializer.increase_container_depth()
454− serializer.serialize_variant_index(1)
455− serializer.serialize_str(value)
456− serializer.decrease_container_depth()
457− }
458−
459− companion object {
460− fun deserialize(deserializer: Deserializer): Io {
461− deserializer.increase_container_depth()
462− val value = deserializer.deserialize_str()
463− deserializer.decrease_container_depth()
464− return Io(value)
465− }
466− }
467− }
468−
469− /// The request timed out before a response was received.
470− data object Timeout: HttpError {
471− override fun serialize(serializer: Serializer) {
472− serializer.increase_container_depth()
473− serializer.serialize_variant_index(2)
474− serializer.decrease_container_depth()
475− }
476−
477− fun deserialize(deserializer: Deserializer): Timeout {
478− return Timeout
479− }
480− }
481−
482− companion object {
483− @Throws(DeserializationError::class)
484− fun deserialize(deserializer: Deserializer): HttpError {
485− val index = deserializer.deserialize_variant_index()
486− return when (index) {
487− 0 -> Url.deserialize(deserializer)
488− 1 -> Io.deserialize(deserializer)
489− 2 -> Timeout.deserialize(deserializer)
490− else -> throw DeserializationError("Unknown variant index for HttpError: $index")
491− }
492− }
493−
494− @Throws(DeserializationError::class)
495− fun bincodeDeserialize(input: ByteArray?): HttpError {
496− if (input == null) {
497− throw DeserializationError("Cannot deserialize null array")
498− }
499− val deserializer = BincodeDeserializer(input)
500− val value = deserialize(deserializer)
501− if (deserializer.get_buffer_offset() < input.size) {
502− throw DeserializationError("Some input bytes were not read")
503− }
504− return value
505− }
506− }
507−}
508−
509−data class HttpHeader(
510− val name: String,
511− val value: String,
512−) {
513− fun serialize(serializer: Serializer) {
514− serializer.increase_container_depth()
515− serializer.serialize_str(name)
516− serializer.serialize_str(value)
517− serializer.decrease_container_depth()
518− }
519−
520− fun bincodeSerialize(): ByteArray {
521− val serializer = BincodeSerializer()
522− serialize(serializer)
523− return serializer.get_bytes()
524− }
525−
526− companion object {
527− fun deserialize(deserializer: Deserializer): HttpHeader {
528− deserializer.increase_container_depth()
529− val name = deserializer.deserialize_str()
530− val value = deserializer.deserialize_str()
531− deserializer.decrease_container_depth()
532− return HttpHeader(name, value)
533− }
534−
535− @Throws(DeserializationError::class)
536− fun bincodeDeserialize(input: ByteArray?): HttpHeader {
537− if (input == null) {
538− throw DeserializationError("Cannot deserialize null array")
539− }
540− val deserializer = BincodeDeserializer(input)
541− val value = deserialize(deserializer)
542− if (deserializer.get_buffer_offset() < input.size) {
543− throw DeserializationError("Some input bytes were not read")
544− }
545− return value
546− }
547− }
548−}
549−
550−/// A raw HTTP request, as sent to the shell over the protocol boundary.
551−///
552−/// # No header validation
553−///
554−/// All fields are plain strings. Header names and values are carried as-is with no
555−/// validation against the HTTP specification. This is intentional: `HttpRequest` is a
556−/// cross-language data carrier deserialised by Swift, Kotlin, and TypeScript shells;
557−/// Rust's `http`-crate validation rules cannot be enforced on the other side of that
558−/// boundary.
559−///
560−/// **Shell authors must not assume that header names or values are well-formed.**
561−/// Pass them to your underlying HTTP client as-is — it will apply its own rules.
562−///
563−/// For the ergonomic Rust-side builder that *does* validate header values (and panics
564−/// on invalid input), see [`crate::command::RequestBuilder`].
565−data class HttpRequest(
566− val method: String,
567− val url: String,
568− val headers: List<soy.iko.opencode.core.HttpHeader>,
569− val body: Bytes,
570−) {
571− fun serialize(serializer: Serializer) {
572− serializer.increase_container_depth()
573− serializer.serialize_str(method)
574− serializer.serialize_str(url)
575− headers.serialize(serializer) {
576− it.serialize(serializer)
577− }
578− serializer.serialize_bytes(body)
579− serializer.decrease_container_depth()
580− }
581−
582− fun bincodeSerialize(): ByteArray {
583− val serializer = BincodeSerializer()
584− serialize(serializer)
585− return serializer.get_bytes()
586− }
587−
588− companion object {
589− fun deserialize(deserializer: Deserializer): HttpRequest {
590− deserializer.increase_container_depth()
591− val method = deserializer.deserialize_str()
592− val url = deserializer.deserialize_str()
593− val headers =
594− deserializer.deserializeListOf {
595− soy.iko.opencode.core.HttpHeader.deserialize(deserializer)
596− }
597− val body = deserializer.deserialize_bytes()
598− deserializer.decrease_container_depth()
599− return HttpRequest(method, url, headers, body)
600− }
601−
602− @Throws(DeserializationError::class)
603− fun bincodeDeserialize(input: ByteArray?): HttpRequest {
604− if (input == null) {
605− throw DeserializationError("Cannot deserialize null array")
606− }
607− val deserializer = BincodeDeserializer(input)
608− val value = deserialize(deserializer)
609− if (deserializer.get_buffer_offset() < input.size) {
610− throw DeserializationError("Some input bytes were not read")
611− }
612− return value
613− }
614− }
615−}
616−
617−data class HttpResponse(
618− val status: UShort,
619− val headers: List<soy.iko.opencode.core.HttpHeader>,
620− val body: Bytes,
621−) {
622− fun serialize(serializer: Serializer) {
623− serializer.increase_container_depth()
624− serializer.serialize_u16(status)
625− headers.serialize(serializer) {
626− it.serialize(serializer)
627− }
628− serializer.serialize_bytes(body)
629− serializer.decrease_container_depth()
630− }
631−
632− fun bincodeSerialize(): ByteArray {
633− val serializer = BincodeSerializer()
634− serialize(serializer)
635− return serializer.get_bytes()
636− }
637−
638− companion object {
639− fun deserialize(deserializer: Deserializer): HttpResponse {
640− deserializer.increase_container_depth()
641− val status = deserializer.deserialize_u16()
642− val headers =
643− deserializer.deserializeListOf {
644− soy.iko.opencode.core.HttpHeader.deserialize(deserializer)
645− }
646− val body = deserializer.deserialize_bytes()
647− deserializer.decrease_container_depth()
648− return HttpResponse(status, headers, body)
649− }
650−
651− @Throws(DeserializationError::class)
652− fun bincodeDeserialize(input: ByteArray?): HttpResponse {
653− if (input == null) {
654− throw DeserializationError("Cannot deserialize null array")
655− }
656− val deserializer = BincodeDeserializer(input)
657− val value = deserialize(deserializer)
658− if (deserializer.get_buffer_offset() < input.size) {
659− throw DeserializationError("Some input bytes were not read")
660− }
661− return value
662− }
663− }
664−}
665−
666−/// The result of an HTTP request, as returned by the shell over the protocol boundary.
667−///
668−/// # Status codes are not errors
669−///
670−/// Any completed HTTP exchange — including responses with 4xx or 5xx status codes — is
671−/// returned as [`HttpResult::Ok`]. Only *transport-level* failures (the shell could not
672−/// reach the server at all) produce [`HttpResult::Err`].
673−///
674−/// To act on an error status, inspect [`HttpResponse::status`]:
675−///
676−/// ```
677−/// # use crux_http::protocol::{HttpResult, HttpResponse};
678−/// # use crux_http::HttpError;
679−/// # fn handle(result: HttpResult) {
680−/// match result {
681−/// HttpResult::Ok(response) if response.status == 200 => { /* success */ }
682−/// HttpResult::Ok(response) if response.status == 404 => { /* not found */ }
683−/// HttpResult::Ok(response) if response.status >= 500 => { /* server error */ }
684−/// HttpResult::Ok(_) => { /* other status */ }
685−/// HttpResult::Err(e) => { /* transport failure: bad URL, IO error, or timeout */ }
686−/// }
687−/// # }
688−/// ```
689−sealed interface HttpResult {
690− fun serialize(serializer: Serializer)
691−
692− fun bincodeSerialize(): ByteArray {
693− val serializer = BincodeSerializer()
694− serialize(serializer)
695− return serializer.get_bytes()
696− }
697−
698− /// The shell completed the HTTP exchange. The response may carry any status code,
699− /// including 4xx and 5xx — inspect [`HttpResponse::status`] to distinguish them.
700− data class Ok(
701− val value: soy.iko.opencode.core.HttpResponse,
702− ) : HttpResult {
703− override fun serialize(serializer: Serializer) {
704− serializer.increase_container_depth()
705− serializer.serialize_variant_index(0)
706− value.serialize(serializer)
707− serializer.decrease_container_depth()
708− }
709−
710− companion object {
711− fun deserialize(deserializer: Deserializer): Ok {
712− deserializer.increase_container_depth()
713− val value = soy.iko.opencode.core.HttpResponse.deserialize(deserializer)
714− deserializer.decrease_container_depth()
715− return Ok(value)
716− }
717− }
718− }
719−
720− /// The shell could not complete the HTTP exchange due to a transport-level failure.
721− /// See [`HttpError`] for the possible causes.
722− data class Err(
723− val value: soy.iko.opencode.core.HttpError,
724− ) : HttpResult {
725− override fun serialize(serializer: Serializer) {
726− serializer.increase_container_depth()
727− serializer.serialize_variant_index(1)
728− value.serialize(serializer)
729− serializer.decrease_container_depth()
730− }
731−
732− companion object {
733− fun deserialize(deserializer: Deserializer): Err {
734− deserializer.increase_container_depth()
735− val value = soy.iko.opencode.core.HttpError.deserialize(deserializer)
736− deserializer.decrease_container_depth()
737− return Err(value)
738− }
739− }
740− }
741−
742− companion object {
743− @Throws(DeserializationError::class)
744− fun deserialize(deserializer: Deserializer): HttpResult {
745− val index = deserializer.deserialize_variant_index()
746− return when (index) {
747− 0 -> Ok.deserialize(deserializer)
748− 1 -> Err.deserialize(deserializer)
749− else -> throw DeserializationError("Unknown variant index for HttpResult: $index")
750− }
751− }
752−
753− @Throws(DeserializationError::class)
754− fun bincodeDeserialize(input: ByteArray?): HttpResult {
755− if (input == null) {
756− throw DeserializationError("Cannot deserialize null array")
757− }
758− val deserializer = BincodeDeserializer(input)
759− val value = deserialize(deserializer)
760− if (deserializer.get_buffer_offset() < input.size) {
761− throw DeserializationError("Some input bytes were not read")
762− }
763− return value
764− }
765− }
766−}
767−
768−data class MessageView(
769− val id: String,
770− val role: String,
771− val text: String,
772− val time: ULong,
773−) {
774− fun serialize(serializer: Serializer) {
775− serializer.increase_container_depth()
776− serializer.serialize_str(id)
777− serializer.serialize_str(role)
778− serializer.serialize_str(text)
779− serializer.serialize_u64(time)
780− serializer.decrease_container_depth()
781− }
782−
783− fun bincodeSerialize(): ByteArray {
784− val serializer = BincodeSerializer()
785− serialize(serializer)
786− return serializer.get_bytes()
787− }
788−
789− companion object {
790− fun deserialize(deserializer: Deserializer): MessageView {
791− deserializer.increase_container_depth()
792− val id = deserializer.deserialize_str()
793− val role = deserializer.deserialize_str()
794− val text = deserializer.deserialize_str()
795− val time = deserializer.deserialize_u64()
796− deserializer.decrease_container_depth()
797− return MessageView(id, role, text, time)
798− }
799−
800− @Throws(DeserializationError::class)
801− fun bincodeDeserialize(input: ByteArray?): MessageView {
802− if (input == null) {
803− throw DeserializationError("Cannot deserialize null array")
804− }
805− val deserializer = BincodeDeserializer(input)
806− val value = deserialize(deserializer)
807− if (deserializer.get_buffer_offset() < input.size) {
808− throw DeserializationError("Some input bytes were not read")
809− }
810− return value
811− }
812− }
813−}
814−
815−/// The single operation `Render` implements.
816−data object RenderOperation {
817− fun serialize(serializer: Serializer) {}
818−
819− fun bincodeSerialize(): ByteArray {
820− val serializer = BincodeSerializer()
821− serialize(serializer)
822− return serializer.get_bytes()
823− }
824−
825− fun deserialize(deserializer: Deserializer): RenderOperation {
826− return RenderOperation
827− }
828−
829− @Throws(DeserializationError::class)
830− fun bincodeDeserialize(input: ByteArray?): RenderOperation {
831− if (input == null) {
832− throw DeserializationError("Cannot deserialize null array")
833− }
834− val deserializer = BincodeDeserializer(input)
835− val value = deserialize(deserializer)
836− if (deserializer.get_buffer_offset() < input.size) {
837− throw DeserializationError("Some input bytes were not read")
838− }
839− return value
840− }
841−}
842−
843−/// Request for a side-effect passed from the Core to the Shell.
844−///
845−/// The `EffectId` links the `Request` with the corresponding call to [`Core::resolve`] to pass the data back
846−/// to the [`App::update`] function (wrapped in the event provided to the capability originating the effect).
847−data class Request(
848− val id: UInt,
849− val effect: soy.iko.opencode.core.Effect,
850−) {
851− fun serialize(serializer: Serializer) {
852− serializer.increase_container_depth()
853− serializer.serialize_u32(id)
854− effect.serialize(serializer)
855− serializer.decrease_container_depth()
856− }
857−
858− fun bincodeSerialize(): ByteArray {
859− val serializer = BincodeSerializer()
860− serialize(serializer)
861− return serializer.get_bytes()
862− }
863−
864− companion object {
865− fun deserialize(deserializer: Deserializer): Request {
866− deserializer.increase_container_depth()
867− val id = deserializer.deserialize_u32()
868− val effect = soy.iko.opencode.core.Effect.deserialize(deserializer)
869− deserializer.decrease_container_depth()
870− return Request(id, effect)
871− }
872−
873− @Throws(DeserializationError::class)
874− fun bincodeDeserialize(input: ByteArray?): Request {
875− if (input == null) {
876− throw DeserializationError("Cannot deserialize null array")
877− }
878− val deserializer = BincodeDeserializer(input)
879− val value = deserialize(deserializer)
880− if (deserializer.get_buffer_offset() < input.size) {
881− throw DeserializationError("Some input bytes were not read")
882− }
883− return value
884− }
885− }
886−}
887−
888−/// A batch of effect requests from the Core to the Shell, as serialised by
889−/// [`Bridge::update`] and [`Bridge::resolve`].
890−///
891−/// The wire format is identical to `Vec<Request<Eff>>` (the newtype is
892−/// `serde(transparent)`), so existing shell code that already deserialises
893−/// a `Vec<Request>` remains binary-compatible.
894−///
895−/// Registering this type with the type-generation system causes the code
896−/// generators to emit a `Requests` type (with a `value` field containing the
897−/// list) together with a top-level `bincodeDeserialize` / `BincodeDeserialize`
898−/// helper, replacing the hand-written extension files that were previously
899−/// appended by `add_extensions()`.
900−data class Requests(
901− val value: List<soy.iko.opencode.core.Request>,
902−) {
903− fun serialize(serializer: Serializer) {
904− serializer.increase_container_depth()
905− value.serialize(serializer) {
906− it.serialize(serializer)
907− }
908− serializer.decrease_container_depth()
909− }
910−
911− fun bincodeSerialize(): ByteArray {
912− val serializer = BincodeSerializer()
913− serialize(serializer)
914− return serializer.get_bytes()
915− }
916−
917− companion object {
918− fun deserialize(deserializer: Deserializer): Requests {
919− deserializer.increase_container_depth()
920− val value =
921− deserializer.deserializeListOf {
922− soy.iko.opencode.core.Request.deserialize(deserializer)
923− }
924− deserializer.decrease_container_depth()
925− return Requests(value)
926− }
927−
928− @Throws(DeserializationError::class)
929− fun bincodeDeserialize(input: ByteArray?): Requests {
930− if (input == null) {
931− throw DeserializationError("Cannot deserialize null array")
932− }
933− val deserializer = BincodeDeserializer(input)
934− val value = deserialize(deserializer)
935− if (deserializer.get_buffer_offset() < input.size) {
936− throw DeserializationError("Some input bytes were not read")
937− }
938− return value
939− }
940− }
941−}
942−
943−enum class Screen {
944− CONNECT,
945− SESSIONS,
946− CHAT;
947−
948− fun serialize(serializer: Serializer) {
949− serializer.increase_container_depth()
950− serializer.serialize_variant_index(ordinal)
951− serializer.decrease_container_depth()
952− }
953−
954− fun bincodeSerialize(): ByteArray {
955− val serializer = BincodeSerializer()
956− serialize(serializer)
957− return serializer.get_bytes()
958− }
959−
960− companion object {
961− @Throws(DeserializationError::class)
962− fun deserialize(deserializer: Deserializer): Screen {
963− deserializer.increase_container_depth()
964− val index = deserializer.deserialize_variant_index()
965− deserializer.decrease_container_depth()
966− return when (index) {
967− 0 -> CONNECT
968− 1 -> SESSIONS
969− 2 -> CHAT
970− else -> throw DeserializationError("Unknown variant index for Screen: $index")
971− }
972− }
973−
974− @Throws(DeserializationError::class)
975− fun bincodeDeserialize(input: ByteArray?): Screen {
976− if (input == null) {
977− throw DeserializationError("Cannot deserialize null array")
978− }
979− val deserializer = BincodeDeserializer(input)
980− val value = deserialize(deserializer)
981− if (deserializer.get_buffer_offset() < input.size) {
982− throw DeserializationError("Some input bytes were not read")
983− }
984− return value
985− }
986− }
987−}
988−
989−data class SessionView(
990− val id: String,
991− val title: String,
992−) {
993− fun serialize(serializer: Serializer) {
994− serializer.increase_container_depth()
995− serializer.serialize_str(id)
996− serializer.serialize_str(title)
997− serializer.decrease_container_depth()
998− }
999−
1000− fun bincodeSerialize(): ByteArray {
1001− val serializer = BincodeSerializer()
1002− serialize(serializer)
1003− return serializer.get_bytes()
1004− }
1005−
1006− companion object {
1007− fun deserialize(deserializer: Deserializer): SessionView {
1008− deserializer.increase_container_depth()
1009− val id = deserializer.deserialize_str()
1010− val title = deserializer.deserialize_str()
1011− deserializer.decrease_container_depth()
1012− return SessionView(id, title)
1013− }
1014−
1015− @Throws(DeserializationError::class)
1016− fun bincodeDeserialize(input: ByteArray?): SessionView {
1017− if (input == null) {
1018− throw DeserializationError("Cannot deserialize null array")
1019− }
1020− val deserializer = BincodeDeserializer(input)
1021− val value = deserialize(deserializer)
1022− if (deserializer.get_buffer_offset() < input.size) {
1023− throw DeserializationError("Some input bytes were not read")
1024− }
1025− return value
1026− }
1027− }
1028−}
1029−
1030−data class ViewModel(
1031− val screen: soy.iko.opencode.core.Screen,
1032− val serverUrl: String,
1033− val connected: Boolean,
1034− val loading: Boolean,
1035− val error: String? = null,
1036− val sessions: List<soy.iko.opencode.core.SessionView>,
1037− val currentSessionId: String? = null,
1038− val messages: List<soy.iko.opencode.core.MessageView>,
1039− val draftMessage: String,
1040−) {
1041− fun serialize(serializer: Serializer) {
1042− serializer.increase_container_depth()
1043− screen.serialize(serializer)
1044− serializer.serialize_str(serverUrl)
1045− serializer.serialize_bool(connected)
1046− serializer.serialize_bool(loading)
1047− error.serializeOptionOf(serializer) {
1048− serializer.serialize_str(it)
1049− }
1050− sessions.serialize(serializer) {
1051− it.serialize(serializer)
1052− }
1053− currentSessionId.serializeOptionOf(serializer) {
1054− serializer.serialize_str(it)
1055− }
1056− messages.serialize(serializer) {
1057− it.serialize(serializer)
1058− }
1059− serializer.serialize_str(draftMessage)
1060− serializer.decrease_container_depth()
1061− }
1062−
1063− fun bincodeSerialize(): ByteArray {
1064− val serializer = BincodeSerializer()
1065− serialize(serializer)
1066− return serializer.get_bytes()
1067− }
1068−
1069− companion object {
1070− fun deserialize(deserializer: Deserializer): ViewModel {
1071− deserializer.increase_container_depth()
1072− val screen = soy.iko.opencode.core.Screen.deserialize(deserializer)
1073− val serverUrl = deserializer.deserialize_str()
1074− val connected = deserializer.deserialize_bool()
1075− val loading = deserializer.deserialize_bool()
1076− val error =
1077− deserializer.deserializeOptionOf {
1078− deserializer.deserialize_str()
1079− }
1080− val sessions =
1081− deserializer.deserializeListOf {
1082− soy.iko.opencode.core.SessionView.deserialize(deserializer)
1083− }
1084− val currentSessionId =
1085− deserializer.deserializeOptionOf {
1086− deserializer.deserialize_str()
1087− }
1088− val messages =
1089− deserializer.deserializeListOf {
1090− soy.iko.opencode.core.MessageView.deserialize(deserializer)
1091− }
1092− val draftMessage = deserializer.deserialize_str()
1093− deserializer.decrease_container_depth()
1094− return ViewModel(screen, serverUrl, connected, loading, error, sessions, currentSessionId, messages, draftMessage)
1095− }
1096−
1097− @Throws(DeserializationError::class)
1098− fun bincodeDeserialize(input: ByteArray?): ViewModel {
1099− if (input == null) {
1100− throw DeserializationError("Cannot deserialize null array")
1101− }
1102− val deserializer = BincodeDeserializer(input)
1103− val value = deserialize(deserializer)
1104− if (deserializer.get_buffer_offset() < input.size) {
1105− throw DeserializationError("Some input bytes were not read")
1106− }
1107− return value
1108− }
1109− }
1110−}
gradle.propertiesdeleted−3
@@ -1,3 +0,0 @@
1−org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
2−android.useAndroidX=true
3−android.suppressUnsupportedCompileSdk=37.0
gradle/libs.versions.tomldeleted−50
@@ -1,50 +0,0 @@
1−[versions]
2−agp = "8.13.2"
3−kotlin = "2.3.0"
4−coreKtx = "1.17.0"
5−junit = "4.13.2"
6−junitVersion = "1.3.0"
7−espressoCore = "3.7.0"
8−lifecycleRuntimeKtx = "2.10.0"
9−activityCompose = "1.12.3"
10−composeBom = "2026.01.01"
11−lifecycle = "2.10.0"
12−ktor = "3.1.3"
13−kotlinxCoroutines = "1.10.2"
14−navigationCompose = "2.9.8"
15−
16−[libraries]
17−androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
18−junit = { group = "junit", name = "junit", version.ref = "junit" }
19−androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
20−androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
21−androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
22−androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
23−androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
24−androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" }
25−androidx-compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
26−androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
27−androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
28−androidx-compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
29−androidx-compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
30−androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" }
31−androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
32−lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycle" }
33−androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
34−
35−ktor-client-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
36−ktor-client-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
37−ktor-client-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
38−ktor-serialization-kotlinx-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }
39−ktor-client-logging = { group = "io.ktor", name = "ktor-client-logging", version.ref = "ktor" }
40−ktor-client-auth = { group = "io.ktor", name = "ktor-client-auth", version.ref = "ktor" }
41−
42−kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
43−kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinxCoroutines" }
44−
45−[plugins]
46−android-application = { id = "com.android.application", version.ref = "agp" }
47−android-library = { id = "com.android.library", version.ref = "agp" }
48−kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
49−kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
50−kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
gradle/wrapper/gradle-wrapper.jardeleted

Binary file not shown.

gradle/wrapper/gradle-wrapper.propertiesdeleted−7
@@ -1,7 +0,0 @@
1−distributionBase=GRADLE_USER_HOME
2−distributionPath=wrapper/dists
3−distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip
4−networkTimeout=10000
5−validateDistributionUrl=true
6−zipStoreBase=GRADLE_USER_HOME
7−zipStorePath=wrapper/dists
gradlewdeleted−251
@@ -1,251 +0,0 @@
1−#!/bin/sh
2−
3−#
4−# Copyright © 2015-2021 the original authors.
5−#
6−# Licensed under the Apache License, Version 2.0 (the "License");
7−# you may not use this file except in compliance with the License.
8−# You may obtain a copy of the License at
9−#
10−# https://www.apache.org/licenses/LICENSE-2.0
11−#
12−# Unless required by applicable law or agreed to in writing, software
13−# distributed under the License is distributed on an "AS IS" BASIS,
14−# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15−# See the License for the specific language governing permissions and
16−# limitations under the License.
17−#
18−# SPDX-License-Identifier: Apache-2.0
19−#
20−
21−##############################################################################
22−#
23−# Gradle start up script for POSIX generated by Gradle.
24−#
25−# Important for running:
26−#
27−# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28−# noncompliant, but you have some other compliant shell such as ksh or
29−# bash, then to run this script, type that shell name before the whole
30−# command line, like:
31−#
32−# ksh Gradle
33−#
34−# Busybox and similar reduced shells will NOT work, because this script
35−# requires all of these POSIX shell features:
36−# * functions;
37−# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38−# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39−# * compound commands having a testable exit status, especially «case»;
40−# * various built-in commands including «command», «set», and «ulimit».
41−#
42−# Important for patching:
43−#
44−# (2) This script targets any POSIX shell, so it avoids extensions provided
45−# by Bash, Ksh, etc; in particular arrays are avoided.
46−#
47−# The "traditional" practice of packing multiple parameters into a
48−# space-separated string is a well documented source of bugs and security
49−# problems, so this is (mostly) avoided, by progressively accumulating
50−# options in "$@", and eventually passing that to Java.
51−#
52−# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53−# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54−# see the in-line comments for details.
55−#
56−# There are tweaks for specific operating systems such as AIX, CygWin,
57−# Darwin, MinGW, and NonStop.
58−#
59−# (3) This script is generated from the Groovy template
60−# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61−# within the Gradle project.
62−#
63−# You can find Gradle at https://github.com/gradle/gradle/.
64−#
65−##############################################################################
66−
67−# Attempt to set APP_HOME
68−
69−# Resolve links: $0 may be a link
70−app_path=$0
71−
72−# Need this for daisy-chained symlinks.
73−while
74− APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75− [ -h "$app_path" ]
76−do
77− ls=$( ls -ld "$app_path" )
78− link=${ls#*' -> '}
79− case $link in #(
80− /*) app_path=$link ;; #(
81− *) app_path=$APP_HOME$link ;;
82− esac
83−done
84−
85−# This is normally unused
86−# shellcheck disable=SC2034
87−APP_BASE_NAME=${0##*/}
88−# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89−APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
90−
91−# Use the maximum available, or set MAX_FD != -1 to use that value.
92−MAX_FD=maximum
93−
94−warn () {
95− echo "$*"
96−} >&2
97−
98−die () {
99− echo
100− echo "$*"
101− echo
102− exit 1
103−} >&2
104−
105−# OS specific support (must be 'true' or 'false').
106−cygwin=false
107−msys=false
108−darwin=false
109−nonstop=false
110−case "$( uname )" in #(
111− CYGWIN* ) cygwin=true ;; #(
112− Darwin* ) darwin=true ;; #(
113− MSYS* | MINGW* ) msys=true ;; #(
114− NONSTOP* ) nonstop=true ;;
115−esac
116−
117−CLASSPATH="\\\"\\\""
118−
119−
120−# Determine the Java command to use to start the JVM.
121−if [ -n "$JAVA_HOME" ] ; then
122− if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123− # IBM's JDK on AIX uses strange locations for the executables
124− JAVACMD=$JAVA_HOME/jre/sh/java
125− else
126− JAVACMD=$JAVA_HOME/bin/java
127− fi
128− if [ ! -x "$JAVACMD" ] ; then
129− die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130−
131−Please set the JAVA_HOME variable in your environment to match the
132−location of your Java installation."
133− fi
134−else
135− JAVACMD=java
136− if ! command -v java >/dev/null 2>&1
137− then
138− die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
139−
140−Please set the JAVA_HOME variable in your environment to match the
141−location of your Java installation."
142− fi
143−fi
144−
145−# Increase the maximum file descriptors if we can.
146−if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
147− case $MAX_FD in #(
148− max*)
149− # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
150− # shellcheck disable=SC2039,SC3045
151− MAX_FD=$( ulimit -H -n ) ||
152− warn "Could not query maximum file descriptor limit"
153− esac
154− case $MAX_FD in #(
155− '' | soft) :;; #(
156− *)
157− # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
158− # shellcheck disable=SC2039,SC3045
159− ulimit -n "$MAX_FD" ||
160− warn "Could not set maximum file descriptor limit to $MAX_FD"
161− esac
162−fi
163−
164−# Collect all arguments for the java command, stacking in reverse order:
165−# * args from the command line
166−# * the main class name
167−# * -classpath
168−# * -D...appname settings
169−# * --module-path (only if needed)
170−# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
171−
172−# For Cygwin or MSYS, switch paths to Windows format before running java
173−if "$cygwin" || "$msys" ; then
174− APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
175− CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
176−
177− JAVACMD=$( cygpath --unix "$JAVACMD" )
178−
179− # Now convert the arguments - kludge to limit ourselves to /bin/sh
180− for arg do
181− if
182− case $arg in #(
183− -*) false ;; # don't mess with options #(
184− /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
185− [ -e "$t" ] ;; #(
186− *) false ;;
187− esac
188− then
189− arg=$( cygpath --path --ignore --mixed "$arg" )
190− fi
191− # Roll the args list around exactly as many times as the number of
192− # args, so each arg winds up back in the position where it started, but
193− # possibly modified.
194− #
195− # NB: a `for` loop captures its iteration list before it begins, so
196− # changing the positional parameters here affects neither the number of
197− # iterations, nor the values presented in `arg`.
198− shift # remove old arg
199− set -- "$@" "$arg" # push replacement arg
200− done
201−fi
202−
203−
204−# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
205−DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
206−
207−# Collect all arguments for the java command:
208−# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
209−# and any embedded shellness will be escaped.
210−# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
211−# treated as '${Hostname}' itself on the command line.
212−
213−set -- \
214− "-Dorg.gradle.appname=$APP_BASE_NAME" \
215− -classpath "$CLASSPATH" \
216− -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
217− "$@"
218−
219−# Stop when "xargs" is not available.
220−if ! command -v xargs >/dev/null 2>&1
221−then
222− die "xargs is not available"
223−fi
224−
225−# Use "xargs" to parse quoted args.
226−#
227−# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
228−#
229−# In Bash we could simply go:
230−#
231−# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
232−# set -- "${ARGS[@]}" "$@"
233−#
234−# but POSIX shell has neither arrays nor command substitution, so instead we
235−# post-process each arg (as a line of input to sed) to backslash-escape any
236−# character that might be a shell metacharacter, then use eval to reverse
237−# that process (while maintaining the separation between arguments), and wrap
238−# the whole thing up as a single "set" statement.
239−#
240−# This will of course break if any of these variables contains a newline or
241−# an unmatched quote.
242−#
243−
244−eval "set -- $(
245− printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
246− xargs -n1 |
247− sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
248− tr '\n' ' '
249− )" '"$@"'
250−
251−exec "$JAVACMD" "$@"
gradlew.batdeleted−94
@@ -1,94 +0,0 @@
1−@rem
2−@rem Copyright 2015 the original author or authors.
3−@rem
4−@rem Licensed under the Apache License, Version 2.0 (the "License");
5−@rem you may not use this file except in compliance with the License.
6−@rem You may obtain a copy of the License at
7−@rem
8−@rem https://www.apache.org/licenses/LICENSE-2.0
9−@rem
10−@rem Unless required by applicable law or agreed to in writing, software
11−@rem distributed under the License is distributed on an "AS IS" BASIS,
12−@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13−@rem See the License for the specific language governing permissions and
14−@rem limitations under the License.
15−@rem
16−@rem SPDX-License-Identifier: Apache-2.0
17−@rem
18−
19−@if "%DEBUG%"=="" @echo off
20−@rem ##########################################################################
21−@rem
22−@rem Gradle startup script for Windows
23−@rem
24−@rem ##########################################################################
25−
26−@rem Set local scope for the variables with windows NT shell
27−if "%OS%"=="Windows_NT" setlocal
28−
29−set DIRNAME=%~dp0
30−if "%DIRNAME%"=="" set DIRNAME=.
31−@rem This is normally unused
32−set APP_BASE_NAME=%~n0
33−set APP_HOME=%DIRNAME%
34−
35−@rem Resolve any "." and ".." in APP_HOME to make it shorter.
36−for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37−
38−@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39−set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40−
41−@rem Find java.exe
42−if defined JAVA_HOME goto findJavaFromJavaHome
43−
44−set JAVA_EXE=java.exe
45−%JAVA_EXE% -version >NUL 2>&1
46−if %ERRORLEVEL% equ 0 goto execute
47−
48−echo. 1>&2
49−echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50−echo. 1>&2
51−echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52−echo location of your Java installation. 1>&2
53−
54−goto fail
55−
56−:findJavaFromJavaHome
57−set JAVA_HOME=%JAVA_HOME:"=%
58−set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59−
60−if exist "%JAVA_EXE%" goto execute
61−
62−echo. 1>&2
63−echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64−echo. 1>&2
65−echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66−echo location of your Java installation. 1>&2
67−
68−goto fail
69−
70−:execute
71−@rem Setup the command line
72−
73−set CLASSPATH=
74−
75−
76−@rem Execute Gradle
77−"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
78−
79−:end
80−@rem End local scope for the variables with windows NT shell
81−if %ERRORLEVEL% equ 0 goto mainEnd
82−
83−:fail
84−rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85−rem the _cmd.exe /c_ return code!
86−set EXIT_CODE=%ERRORLEVEL%
87−if %EXIT_CODE% equ 0 set EXIT_CODE=1
88−if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89−exit /b %EXIT_CODE%
90−
91−:mainEnd
92−if "%OS%"=="Windows_NT" endlocal
93−
94−:omega
settings.gradle.ktsdeleted−25
@@ -1,25 +0,0 @@
1−pluginManagement {
2− repositories {
3− google {
4− content {
5− includeGroupByRegex("com\\.android.*")
6− includeGroupByRegex("com\\.google.*")
7− includeGroupByRegex("androidx.*")
8− }
9− }
10− mavenCentral()
11− gradlePluginPortal()
12− }
13−}
14−
15−dependencyResolutionManagement {
16− repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
17− repositories {
18− google()
19− mavenCentral()
20− }
21−}
22−
23−rootProject.name = "opencode-android"
24−include(":app")
25−include(":shared")
shared/Cargo.lockdeleted−1707
@@ -1,1707 +0,0 @@
1−# This file is automatically @generated by Cargo.
2−# It is not intended for manual editing.
3−version = 4
4−
5−[[package]]
6−name = "aho-corasick"
7−version = "1.1.4"
8−source = "registry+https://github.com/rust-lang/crates.io-index"
9−checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
10−dependencies = [
11− "memchr",
12−]
13−
14−[[package]]
15−name = "allocator-api2"
16−version = "0.2.21"
17−source = "registry+https://github.com/rust-lang/crates.io-index"
18−checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
19−
20−[[package]]
21−name = "anstream"
22−version = "1.0.0"
23−source = "registry+https://github.com/rust-lang/crates.io-index"
24−checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
25−dependencies = [
26− "anstyle",
27− "anstyle-parse",
28− "anstyle-query",
29− "anstyle-wincon",
30− "colorchoice",
31− "is_terminal_polyfill",
32− "utf8parse",
33−]
34−
35−[[package]]
36−name = "anstyle"
37−version = "1.0.14"
38−source = "registry+https://github.com/rust-lang/crates.io-index"
39−checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
40−
41−[[package]]
42−name = "anstyle-parse"
43−version = "1.0.0"
44−source = "registry+https://github.com/rust-lang/crates.io-index"
45−checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
46−dependencies = [
47− "utf8parse",
48−]
49−
50−[[package]]
51−name = "anstyle-query"
52−version = "1.1.5"
53−source = "registry+https://github.com/rust-lang/crates.io-index"
54−checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
55−dependencies = [
56− "windows-sys",
57−]
58−
59−[[package]]
60−name = "anstyle-wincon"
61−version = "3.0.11"
62−source = "registry+https://github.com/rust-lang/crates.io-index"
63−checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
64−dependencies = [
65− "anstyle",
66− "once_cell_polyfill",
67− "windows-sys",
68−]
69−
70−[[package]]
71−name = "anyhow"
72−version = "1.0.103"
73−source = "registry+https://github.com/rust-lang/crates.io-index"
74−checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
75−
76−[[package]]
77−name = "async-trait"
78−version = "0.1.89"
79−source = "registry+https://github.com/rust-lang/crates.io-index"
80−checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
81−dependencies = [
82− "proc-macro2",
83− "quote",
84− "syn 2.0.118",
85−]
86−
87−[[package]]
88−name = "autocfg"
89−version = "1.5.1"
90−source = "registry+https://github.com/rust-lang/crates.io-index"
91−checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
92−
93−[[package]]
94−name = "bincode"
95−version = "1.3.3"
96−source = "registry+https://github.com/rust-lang/crates.io-index"
97−checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
98−dependencies = [
99− "serde",
100−]
101−
102−[[package]]
103−name = "boltffi"
104−version = "0.27.3"
105−source = "registry+https://github.com/rust-lang/crates.io-index"
106−checksum = "a52185e3efe73ae1e37d4a12ecfc51b03392b3bd6fa677b49bf4e6f4eb30d033"
107−dependencies = [
108− "boltffi_core",
109− "boltffi_macros",
110−]
111−
112−[[package]]
113−name = "boltffi_ast"
114−version = "0.27.3"
115−source = "registry+https://github.com/rust-lang/crates.io-index"
116−checksum = "447bff7f815366882b6c7152284cf60b35bda4ee4920c20f9ca702b3de5d517e"
117−dependencies = [
118− "serde",
119−]
120−
121−[[package]]
122−name = "boltffi_binding"
123−version = "0.27.3"
124−source = "registry+https://github.com/rust-lang/crates.io-index"
125−checksum = "f162e2813a4168530ee144a411921c89b2148e2aedda2327b7f9c46cd57e50d0"
126−dependencies = [
127− "boltffi_ast",
128− "serde",
129− "serde_json",
130−]
131−
132−[[package]]
133−name = "boltffi_core"
134−version = "0.27.3"
135−source = "registry+https://github.com/rust-lang/crates.io-index"
136−checksum = "f6df0e010773b88cf3888511de299425dce2ed0458c9f9d02f454e23590ac1e8"
137−dependencies = [
138− "boltffi_macros",
139− "url",
140− "uuid",
141−]
142−
143−[[package]]
144−name = "boltffi_ffi_rules"
145−version = "0.27.3"
146−source = "registry+https://github.com/rust-lang/crates.io-index"
147−checksum = "8d72f6392c860c87c069c1b11c890d208cae4db143f72230d73ce53155ab2370"
148−dependencies = [
149− "serde",
150− "serde_json",
151− "syn 2.0.118",
152− "walkdir",
153−]
154−
155−[[package]]
156−name = "boltffi_macros"
157−version = "0.27.3"
158−source = "registry+https://github.com/rust-lang/crates.io-index"
159−checksum = "8b2f35b6e1b94218d5178916bdd59f0c358214c146f3ce189fc96ddaa019d435"
160−dependencies = [
161− "boltffi_ast",
162− "boltffi_binding",
163− "boltffi_ffi_rules",
164− "boltffi_scan",
165− "proc-macro2",
166− "quote",
167− "syn 2.0.118",
168−]
169−
170−[[package]]
171−name = "boltffi_scan"
172−version = "0.27.3"
173−source = "registry+https://github.com/rust-lang/crates.io-index"
174−checksum = "4476cba632b69b8736ea499a463a8eddc6fd311f320f98601e338ddc0c9476a0"
175−dependencies = [
176− "boltffi_ast",
177− "proc-macro2",
178− "quote",
179− "serde",
180− "serde_json",
181− "syn 2.0.118",
182− "walkdir",
183−]
184−
185−[[package]]
186−name = "bumpalo"
187−version = "3.20.3"
188−source = "registry+https://github.com/rust-lang/crates.io-index"
189−checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
190−
191−[[package]]
192−name = "bytes"
193−version = "1.12.0"
194−source = "registry+https://github.com/rust-lang/crates.io-index"
195−checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593"
196−
197−[[package]]
198−name = "cfg-if"
199−version = "1.0.4"
200−source = "registry+https://github.com/rust-lang/crates.io-index"
201−checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
202−
203−[[package]]
204−name = "clap"
205−version = "4.6.1"
206−source = "registry+https://github.com/rust-lang/crates.io-index"
207−checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
208−dependencies = [
209− "clap_builder",
210− "clap_derive",
211−]
212−
213−[[package]]
214−name = "clap_builder"
215−version = "4.6.0"
216−source = "registry+https://github.com/rust-lang/crates.io-index"
217−checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
218−dependencies = [
219− "anstream",
220− "anstyle",
221− "clap_lex",
222− "strsim",
223−]
224−
225−[[package]]
226−name = "clap_derive"
227−version = "4.6.1"
228−source = "registry+https://github.com/rust-lang/crates.io-index"
229−checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
230−dependencies = [
231− "heck 0.5.0",
232− "proc-macro2",
233− "quote",
234− "syn 2.0.118",
235−]
236−
237−[[package]]
238−name = "clap_lex"
239−version = "1.1.0"
240−source = "registry+https://github.com/rust-lang/crates.io-index"
241−checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
242−
243−[[package]]
244−name = "colorchoice"
245−version = "1.0.5"
246−source = "registry+https://github.com/rust-lang/crates.io-index"
247−checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
248−
249−[[package]]
250−name = "const-fnv1a-hash"
251−version = "1.1.0"
252−source = "registry+https://github.com/rust-lang/crates.io-index"
253−checksum = "32b13ea120a812beba79e34316b3942a857c86ec1593cb34f27bb28272ce2cca"
254−
255−[[package]]
256−name = "crossbeam-channel"
257−version = "0.5.16"
258−source = "registry+https://github.com/rust-lang/crates.io-index"
259−checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
260−dependencies = [
261− "crossbeam-utils",
262−]
263−
264−[[package]]
265−name = "crossbeam-utils"
266−version = "0.8.22"
267−source = "registry+https://github.com/rust-lang/crates.io-index"
268−checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
269−
270−[[package]]
271−name = "crux_core"
272−version = "0.19.0"
273−source = "registry+https://github.com/rust-lang/crates.io-index"
274−checksum = "159720bb44c1a09e0089459217c8ea6c71a78d868c5bf55d5b41f8781e92af7a"
275−dependencies = [
276− "anyhow",
277− "bincode",
278− "crossbeam-channel",
279− "crux_macros",
280− "facet",
281− "facet_generate",
282− "futures",
283− "include_dir 0.7.4",
284− "log",
285− "serde",
286− "serde-generate",
287− "serde-reflection",
288− "serde_json",
289− "slab",
290− "thiserror 2.0.18",
291−]
292−
293−[[package]]
294−name = "crux_http"
295−version = "0.19.0"
296−source = "registry+https://github.com/rust-lang/crates.io-index"
297−checksum = "cb5aec17a4618dc5834f201aaa28f0ea4cad5a5f593714e74a43ff95cf171313"
298−dependencies = [
299− "anyhow",
300− "async-trait",
301− "crux_core",
302− "derive_builder",
303− "encoding_rs",
304− "facet",
305− "facet-generate-attrs",
306− "futures-util",
307− "http",
308− "mime",
309− "pin-project-lite",
310− "serde",
311− "serde_bytes",
312− "serde_json",
313− "serde_qs",
314− "thiserror 2.0.18",
315− "url",
316− "web-sys",
317−]
318−
319−[[package]]
320−name = "crux_macros"
321−version = "0.10.0"
322−source = "registry+https://github.com/rust-lang/crates.io-index"
323−checksum = "952c0b238735c79e917b908c140c7c5f6f6921924ebee21171203fe5fb5fc3f3"
324−dependencies = [
325− "darling 0.23.0",
326− "heck 0.5.0",
327− "proc-macro-error",
328− "proc-macro2",
329− "quote",
330− "syn 2.0.118",
331−]
332−
333−[[package]]
334−name = "darling"
335−version = "0.20.11"
336−source = "registry+https://github.com/rust-lang/crates.io-index"
337−checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
338−dependencies = [
339− "darling_core 0.20.11",
340− "darling_macro 0.20.11",
341−]
342−
343−[[package]]
344−name = "darling"
345−version = "0.23.0"
346−source = "registry+https://github.com/rust-lang/crates.io-index"
347−checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d"
348−dependencies = [
349− "darling_core 0.23.0",
350− "darling_macro 0.23.0",
351−]
352−
353−[[package]]
354−name = "darling_core"
355−version = "0.20.11"
356−source = "registry+https://github.com/rust-lang/crates.io-index"
357−checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
358−dependencies = [
359− "fnv",
360− "ident_case",
361− "proc-macro2",
362− "quote",
363− "strsim",
364− "syn 2.0.118",
365−]
366−
367−[[package]]
368−name = "darling_core"
369−version = "0.23.0"
370−source = "registry+https://github.com/rust-lang/crates.io-index"
371−checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0"
372−dependencies = [
373− "ident_case",
374− "proc-macro2",
375− "quote",
376− "strsim",
377− "syn 2.0.118",
378−]
379−
380−[[package]]
381−name = "darling_macro"
382−version = "0.20.11"
383−source = "registry+https://github.com/rust-lang/crates.io-index"
384−checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
385−dependencies = [
386− "darling_core 0.20.11",
387− "quote",
388− "syn 2.0.118",
389−]
390−
391−[[package]]
392−name = "darling_macro"
393−version = "0.23.0"
394−source = "registry+https://github.com/rust-lang/crates.io-index"
395−checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d"
396−dependencies = [
397− "darling_core 0.23.0",
398− "quote",
399− "syn 2.0.118",
400−]
401−
402−[[package]]
403−name = "derive_builder"
404−version = "0.20.2"
405−source = "registry+https://github.com/rust-lang/crates.io-index"
406−checksum = "507dfb09ea8b7fa618fcf76e953f4f5e192547945816d5358edffe39f6f94947"
407−dependencies = [
408− "derive_builder_macro",
409−]
410−
411−[[package]]
412−name = "derive_builder_core"
413−version = "0.20.2"
414−source = "registry+https://github.com/rust-lang/crates.io-index"
415−checksum = "2d5bcf7b024d6835cfb3d473887cd966994907effbe9227e8c8219824d06c4e8"
416−dependencies = [
417− "darling 0.20.11",
418− "proc-macro2",
419− "quote",
420− "syn 2.0.118",
421−]
422−
423−[[package]]
424−name = "derive_builder_macro"
425−version = "0.20.2"
426−source = "registry+https://github.com/rust-lang/crates.io-index"
427−checksum = "ab63b0e2bf4d5928aff72e83a7dace85d7bba5fe12dcc3c5a572d78caffd3f3c"
428−dependencies = [
429− "derive_builder_core",
430− "syn 2.0.118",
431−]
432−
433−[[package]]
434−name = "displaydoc"
435−version = "0.2.6"
436−source = "registry+https://github.com/rust-lang/crates.io-index"
437−checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
438−dependencies = [
439− "proc-macro2",
440− "quote",
441− "syn 2.0.118",
442−]
443−
444−[[package]]
445−name = "encoding_rs"
446−version = "0.8.35"
447−source = "registry+https://github.com/rust-lang/crates.io-index"
448−checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
449−dependencies = [
450− "cfg-if",
451−]
452−
453−[[package]]
454−name = "equivalent"
455−version = "1.0.2"
456−source = "registry+https://github.com/rust-lang/crates.io-index"
457−checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
458−
459−[[package]]
460−name = "facet"
461−version = "0.44.5"
462−source = "registry+https://github.com/rust-lang/crates.io-index"
463−checksum = "c78066af2cc259674a54fef24323f6081ae54a5f3a3fb53b995af7a867041c81"
464−dependencies = [
465− "autocfg",
466− "facet-core",
467− "facet-macros",
468−]
469−
470−[[package]]
471−name = "facet-core"
472−version = "0.44.4"
473−source = "registry+https://github.com/rust-lang/crates.io-index"
474−checksum = "04625a816bc27bb757b5f62ec17a228b90c08631f579ee0b36eccd6c426df207"
475−dependencies = [
476− "autocfg",
477− "const-fnv1a-hash",
478− "iddqd",
479− "impls",
480−]
481−
482−[[package]]
483−name = "facet-generate-attrs"
484−version = "0.17.0"
485−source = "registry+https://github.com/rust-lang/crates.io-index"
486−checksum = "3b430b495d59ffcf07dabe103a796d8d3ba809d12bb25a5960b605883835b3de"
487−dependencies = [
488− "facet",
489−]
490−
491−[[package]]
492−name = "facet-macro-parse"
493−version = "0.44.4"
494−source = "registry+https://github.com/rust-lang/crates.io-index"
495−checksum = "a288cd230608b4d7e89b307f85be2029f265e27dcdc565a2ae5358d0c4e53f57"
496−dependencies = [
497− "facet-macro-types",
498− "proc-macro2",
499− "quote",
500−]
501−
502−[[package]]
503−name = "facet-macro-types"
504−version = "0.44.4"
505−source = "registry+https://github.com/rust-lang/crates.io-index"
506−checksum = "9c805b7f1ad4bba14e1dba5c67ef810bf9eef2373364fe3916af1d08a5411296"
507−dependencies = [
508− "proc-macro2",
509− "quote",
510− "unsynn",
511−]
512−
513−[[package]]
514−name = "facet-macros"
515−version = "0.44.4"
516−source = "registry+https://github.com/rust-lang/crates.io-index"
517−checksum = "e0cc46a94a2725e82f2c0095bac16665057c24d7f4f11169a7a879dbcba81d8f"
518−dependencies = [
519− "facet-macros-impl",
520−]
521−
522−[[package]]
523−name = "facet-macros-impl"
524−version = "0.44.4"
525−source = "registry+https://github.com/rust-lang/crates.io-index"
526−checksum = "1af13fd5dfa728cc48418cf33cbcfc0d5cfdbaaf22a3d0f5229da4b1e5669fa5"
527−dependencies = [
528− "facet-macro-parse",
529− "facet-macro-types",
530− "proc-macro2",
531− "quote",
532− "strsim",
533− "unsynn",
534−]
535−
536−[[package]]
537−name = "facet_generate"
538−version = "0.17.2"
539−source = "registry+https://github.com/rust-lang/crates.io-index"
540−checksum = "4bab57b923bae6e204f5e47643054a9c5ad9c3bd9aadec66df741790de8a859f"
541−dependencies = [
542− "derive_builder",
543− "facet",
544− "facet-generate-attrs",
545− "heck 0.5.0",
546− "include_dir 0.7.4",
547− "indent",
548− "indoc",
549− "regex",
550− "serde",
551− "serde_json",
552− "textwrap 0.16.2",
553− "thiserror 2.0.18",
554−]
555−
556−[[package]]
557−name = "fnv"
558−version = "1.0.7"
559−source = "registry+https://github.com/rust-lang/crates.io-index"
560−checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
561−
562−[[package]]
563−name = "foldhash"
564−version = "0.2.0"
565−source = "registry+https://github.com/rust-lang/crates.io-index"
566−checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
567−
568−[[package]]
569−name = "form_urlencoded"
570−version = "1.2.2"
571−source = "registry+https://github.com/rust-lang/crates.io-index"
572−checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
573−dependencies = [
574− "percent-encoding",
575−]
576−
577−[[package]]
578−name = "futures"
579−version = "0.3.32"
580−source = "registry+https://github.com/rust-lang/crates.io-index"
581−checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d"
582−dependencies = [
583− "futures-channel",
584− "futures-core",
585− "futures-executor",
586− "futures-io",
587− "futures-sink",
588− "futures-task",
589− "futures-util",
590−]
591−
592−[[package]]
593−name = "futures-channel"
594−version = "0.3.32"
595−source = "registry+https://github.com/rust-lang/crates.io-index"
596−checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
597−dependencies = [
598− "futures-core",
599− "futures-sink",
600−]
601−
602−[[package]]
603−name = "futures-core"
604−version = "0.3.32"
605−source = "registry+https://github.com/rust-lang/crates.io-index"
606−checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
607−
608−[[package]]
609−name = "futures-executor"
610−version = "0.3.32"
611−source = "registry+https://github.com/rust-lang/crates.io-index"
612−checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
613−dependencies = [
614− "futures-core",
615− "futures-task",
616− "futures-util",
617−]
618−
619−[[package]]
620−name = "futures-io"
621−version = "0.3.32"
622−source = "registry+https://github.com/rust-lang/crates.io-index"
623−checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
624−
625−[[package]]
626−name = "futures-macro"
627−version = "0.3.32"
628−source = "registry+https://github.com/rust-lang/crates.io-index"
629−checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
630−dependencies = [
631− "proc-macro2",
632− "quote",
633− "syn 2.0.118",
634−]
635−
636−[[package]]
637−name = "futures-sink"
638−version = "0.3.32"
639−source = "registry+https://github.com/rust-lang/crates.io-index"
640−checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
641−
642−[[package]]
643−name = "futures-task"
644−version = "0.3.32"
645−source = "registry+https://github.com/rust-lang/crates.io-index"
646−checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
647−
648−[[package]]
649−name = "futures-util"
650−version = "0.3.32"
651−source = "registry+https://github.com/rust-lang/crates.io-index"
652−checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
653−dependencies = [
654− "futures-channel",
655− "futures-core",
656− "futures-io",
657− "futures-macro",
658− "futures-sink",
659− "futures-task",
660− "memchr",
661− "pin-project-lite",
662− "slab",
663−]
664−
665−[[package]]
666−name = "getrandom"
667−version = "0.2.17"
668−source = "registry+https://github.com/rust-lang/crates.io-index"
669−checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
670−dependencies = [
671− "cfg-if",
672− "libc",
673− "wasi",
674−]
675−
676−[[package]]
677−name = "glob"
678−version = "0.3.3"
679−source = "registry+https://github.com/rust-lang/crates.io-index"
680−checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
681−
682−[[package]]
683−name = "hashbrown"
684−version = "0.16.1"
685−source = "registry+https://github.com/rust-lang/crates.io-index"
686−checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
687−dependencies = [
688− "allocator-api2",
689−]
690−
691−[[package]]
692−name = "heck"
693−version = "0.3.3"
694−source = "registry+https://github.com/rust-lang/crates.io-index"
695−checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
696−dependencies = [
697− "unicode-segmentation",
698−]
699−
700−[[package]]
701−name = "heck"
702−version = "0.5.0"
703−source = "registry+https://github.com/rust-lang/crates.io-index"
704−checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
705−
706−[[package]]
707−name = "http"
708−version = "1.4.2"
709−source = "registry+https://github.com/rust-lang/crates.io-index"
710−checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425"
711−dependencies = [
712− "bytes",
713− "itoa",
714−]
715−
716−[[package]]
717−name = "icu_collections"
718−version = "2.2.0"
719−source = "registry+https://github.com/rust-lang/crates.io-index"
720−checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
721−dependencies = [
722− "displaydoc",
723− "potential_utf",
724− "utf8_iter",
725− "yoke",
726− "zerofrom",
727− "zerovec",
728−]
729−
730−[[package]]
731−name = "icu_locale_core"
732−version = "2.2.0"
733−source = "registry+https://github.com/rust-lang/crates.io-index"
734−checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
735−dependencies = [
736− "displaydoc",
737− "litemap",
738− "tinystr",
739− "writeable",
740− "zerovec",
741−]
742−
743−[[package]]
744−name = "icu_normalizer"
745−version = "2.2.0"
746−source = "registry+https://github.com/rust-lang/crates.io-index"
747−checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
748−dependencies = [
749− "icu_collections",
750− "icu_normalizer_data",
751− "icu_properties",
752− "icu_provider",
753− "smallvec",
754− "zerovec",
755−]
756−
757−[[package]]
758−name = "icu_normalizer_data"
759−version = "2.2.0"
760−source = "registry+https://github.com/rust-lang/crates.io-index"
761−checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
762−
763−[[package]]
764−name = "icu_properties"
765−version = "2.2.0"
766−source = "registry+https://github.com/rust-lang/crates.io-index"
767−checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
768−dependencies = [
769− "icu_collections",
770− "icu_locale_core",
771− "icu_properties_data",
772− "icu_provider",
773− "zerotrie",
774− "zerovec",
775−]
776−
777−[[package]]
778−name = "icu_properties_data"
779−version = "2.2.0"
780−source = "registry+https://github.com/rust-lang/crates.io-index"
781−checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
782−
783−[[package]]
784−name = "icu_provider"
785−version = "2.2.0"
786−source = "registry+https://github.com/rust-lang/crates.io-index"
787−checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
788−dependencies = [
789− "displaydoc",
790− "icu_locale_core",
791− "writeable",
792− "yoke",
793− "zerofrom",
794− "zerotrie",
795− "zerovec",
796−]
797−
798−[[package]]
799−name = "iddqd"
800−version = "0.3.18"
801−source = "registry+https://github.com/rust-lang/crates.io-index"
802−checksum = "616230c7d641ef971a3a5bfcc654c6b7524ab9c9fb665693c6a397dba9a14aca"
803−dependencies = [
804− "allocator-api2",
805− "equivalent",
806− "foldhash",
807− "hashbrown",
808− "rustc-hash",
809−]
810−
811−[[package]]
812−name = "ident_case"
813−version = "1.0.1"
814−source = "registry+https://github.com/rust-lang/crates.io-index"
815−checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
816−
817−[[package]]
818−name = "idna"
819−version = "1.1.0"
820−source = "registry+https://github.com/rust-lang/crates.io-index"
821−checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
822−dependencies = [
823− "idna_adapter",
824− "smallvec",
825− "utf8_iter",
826−]
827−
828−[[package]]
829−name = "idna_adapter"
830−version = "1.2.2"
831−source = "registry+https://github.com/rust-lang/crates.io-index"
832−checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
833−dependencies = [
834− "icu_normalizer",
835− "icu_properties",
836−]
837−
838−[[package]]
839−name = "impls"
840−version = "1.0.3"
841−source = "registry+https://github.com/rust-lang/crates.io-index"
842−checksum = "7a46645bbd70538861a90d0f26c31537cdf1e44aae99a794fb75a664b70951bc"
843−
844−[[package]]
845−name = "include_dir"
846−version = "0.6.2"
847−source = "registry+https://github.com/rust-lang/crates.io-index"
848−checksum = "24b56e147e6187d61e9d0f039f10e070d0c0a887e24fe0bb9ca3f29bfde62cab"
849−dependencies = [
850− "glob",
851− "include_dir_impl",
852− "proc-macro-hack",
853−]
854−
855−[[package]]
856−name = "include_dir"
857−version = "0.7.4"
858−source = "registry+https://github.com/rust-lang/crates.io-index"
859−checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd"
860−dependencies = [
861− "include_dir_macros",
862−]
863−
864−[[package]]
865−name = "include_dir_impl"
866−version = "0.6.2"
867−source = "registry+https://github.com/rust-lang/crates.io-index"
868−checksum = "0a0c890c85da4bab7bce4204c707396bbd3c6c8a681716a51c8814cfc2b682df"
869−dependencies = [
870− "anyhow",
871− "proc-macro-hack",
872− "proc-macro2",
873− "quote",
874− "syn 1.0.109",
875−]
876−
877−[[package]]
878−name = "include_dir_macros"
879−version = "0.7.4"
880−source = "registry+https://github.com/rust-lang/crates.io-index"
881−checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75"
882−dependencies = [
883− "proc-macro2",
884− "quote",
885−]
886−
887−[[package]]
888−name = "indent"
889−version = "0.1.1"
890−source = "registry+https://github.com/rust-lang/crates.io-index"
891−checksum = "d9f1a0777d972970f204fdf8ef319f1f4f8459131636d7e3c96c5d59570d0fa6"
892−
893−[[package]]
894−name = "indoc"
895−version = "2.0.7"
896−source = "registry+https://github.com/rust-lang/crates.io-index"
897−checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706"
898−dependencies = [
899− "rustversion",
900−]
901−
902−[[package]]
903−name = "is_terminal_polyfill"
904−version = "1.70.2"
905−source = "registry+https://github.com/rust-lang/crates.io-index"
906−checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
907−
908−[[package]]
909−name = "itoa"
910−version = "1.0.18"
911−source = "registry+https://github.com/rust-lang/crates.io-index"
912−checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
913−
914−[[package]]
915−name = "js-sys"
916−version = "0.3.103"
917−source = "registry+https://github.com/rust-lang/crates.io-index"
918−checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
919−dependencies = [
920− "cfg-if",
921− "futures-util",
922− "wasm-bindgen",
923−]
924−
925−[[package]]
926−name = "libc"
927−version = "0.2.186"
928−source = "registry+https://github.com/rust-lang/crates.io-index"
929−checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
930−
931−[[package]]
932−name = "litemap"
933−version = "0.8.2"
934−source = "registry+https://github.com/rust-lang/crates.io-index"
935−checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
936−
937−[[package]]
938−name = "log"
939−version = "0.4.33"
940−source = "registry+https://github.com/rust-lang/crates.io-index"
941−checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
942−
943−[[package]]
944−name = "memchr"
945−version = "2.8.3"
946−source = "registry+https://github.com/rust-lang/crates.io-index"
947−checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
948−
949−[[package]]
950−name = "mime"
951−version = "0.3.17"
952−source = "registry+https://github.com/rust-lang/crates.io-index"
953−checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
954−
955−[[package]]
956−name = "mutants"
957−version = "0.0.3"
958−source = "registry+https://github.com/rust-lang/crates.io-index"
959−checksum = "bc0287524726960e07b119cebd01678f852f147742ae0d925e6a520dca956126"
960−
961−[[package]]
962−name = "once_cell"
963−version = "1.21.4"
964−source = "registry+https://github.com/rust-lang/crates.io-index"
965−checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
966−
967−[[package]]
968−name = "once_cell_polyfill"
969−version = "1.70.2"
970−source = "registry+https://github.com/rust-lang/crates.io-index"
971−checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
972−
973−[[package]]
974−name = "percent-encoding"
975−version = "2.3.2"
976−source = "registry+https://github.com/rust-lang/crates.io-index"
977−checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
978−
979−[[package]]
980−name = "phf"
981−version = "0.10.1"
982−source = "registry+https://github.com/rust-lang/crates.io-index"
983−checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
984−dependencies = [
985− "phf_macros",
986− "phf_shared",
987− "proc-macro-hack",
988−]
989−
990−[[package]]
991−name = "phf_generator"
992−version = "0.10.0"
993−source = "registry+https://github.com/rust-lang/crates.io-index"
994−checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
995−dependencies = [
996− "phf_shared",
997− "rand",
998−]
999−
1000−[[package]]
1001−name = "phf_macros"
1002−version = "0.10.0"
1003−source = "registry+https://github.com/rust-lang/crates.io-index"
1004−checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0"
1005−dependencies = [
1006− "phf_generator",
1007− "phf_shared",
1008− "proc-macro-hack",
1009− "proc-macro2",
1010− "quote",
1011− "syn 1.0.109",
1012−]
1013−
1014−[[package]]
1015−name = "phf_shared"
1016−version = "0.10.0"
1017−source = "registry+https://github.com/rust-lang/crates.io-index"
1018−checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
1019−dependencies = [
1020− "siphasher",
1021−]
1022−
1023−[[package]]
1024−name = "pin-project-lite"
1025−version = "0.2.17"
1026−source = "registry+https://github.com/rust-lang/crates.io-index"
1027−checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
1028−
1029−[[package]]
1030−name = "potential_utf"
1031−version = "0.1.5"
1032−source = "registry+https://github.com/rust-lang/crates.io-index"
1033−checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
1034−dependencies = [
1035− "zerovec",
1036−]
1037−
1038−[[package]]
1039−name = "ppv-lite86"
1040−version = "0.2.21"
1041−source = "registry+https://github.com/rust-lang/crates.io-index"
1042−checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
1043−dependencies = [
1044− "zerocopy",
1045−]
1046−
1047−[[package]]
1048−name = "proc-macro-error"
1049−version = "1.0.4"
1050−source = "registry+https://github.com/rust-lang/crates.io-index"
1051−checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
1052−dependencies = [
1053− "proc-macro-error-attr",
1054− "proc-macro2",
1055− "quote",
1056− "syn 1.0.109",
1057− "version_check",
1058−]
1059−
1060−[[package]]
1061−name = "proc-macro-error-attr"
1062−version = "1.0.4"
1063−source = "registry+https://github.com/rust-lang/crates.io-index"
1064−checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
1065−dependencies = [
1066− "proc-macro2",
1067− "quote",
1068− "version_check",
1069−]
1070−
1071−[[package]]
1072−name = "proc-macro-hack"
1073−version = "0.5.20+deprecated"
1074−source = "registry+https://github.com/rust-lang/crates.io-index"
1075−checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
1076−
1077−[[package]]
1078−name = "proc-macro2"
1079−version = "1.0.106"
1080−source = "registry+https://github.com/rust-lang/crates.io-index"
1081−checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
1082−dependencies = [
1083− "unicode-ident",
1084−]
1085−
1086−[[package]]
1087−name = "quote"
1088−version = "1.0.46"
1089−source = "registry+https://github.com/rust-lang/crates.io-index"
1090−checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
1091−dependencies = [
1092− "proc-macro2",
1093−]
1094−
1095−[[package]]
1096−name = "rand"
1097−version = "0.8.6"
1098−source = "registry+https://github.com/rust-lang/crates.io-index"
1099−checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
1100−dependencies = [
1101− "libc",
1102− "rand_chacha",
1103− "rand_core",
1104−]
1105−
1106−[[package]]
1107−name = "rand_chacha"
1108−version = "0.3.1"
1109−source = "registry+https://github.com/rust-lang/crates.io-index"
1110−checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
1111−dependencies = [
1112− "ppv-lite86",
1113− "rand_core",
1114−]
1115−
1116−[[package]]
1117−name = "rand_core"
1118−version = "0.6.4"
1119−source = "registry+https://github.com/rust-lang/crates.io-index"
1120−checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
1121−dependencies = [
1122− "getrandom",
1123−]
1124−
1125−[[package]]
1126−name = "regex"
1127−version = "1.12.4"
1128−source = "registry+https://github.com/rust-lang/crates.io-index"
1129−checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
1130−dependencies = [
1131− "aho-corasick",
1132− "memchr",
1133− "regex-automata",
1134− "regex-syntax",
1135−]
1136−
1137−[[package]]
1138−name = "regex-automata"
1139−version = "0.4.14"
1140−source = "registry+https://github.com/rust-lang/crates.io-index"
1141−checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
1142−dependencies = [
1143− "aho-corasick",
1144− "memchr",
1145− "regex-syntax",
1146−]
1147−
1148−[[package]]
1149−name = "regex-syntax"
1150−version = "0.8.11"
1151−source = "registry+https://github.com/rust-lang/crates.io-index"
1152−checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
1153−
1154−[[package]]
1155−name = "rustc-hash"
1156−version = "2.1.3"
1157−source = "registry+https://github.com/rust-lang/crates.io-index"
1158−checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
1159−
1160−[[package]]
1161−name = "rustversion"
1162−version = "1.0.23"
1163−source = "registry+https://github.com/rust-lang/crates.io-index"
1164−checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
1165−
1166−[[package]]
1167−name = "ryu"
1168−version = "1.0.23"
1169−source = "registry+https://github.com/rust-lang/crates.io-index"
1170−checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
1171−
1172−[[package]]
1173−name = "same-file"
1174−version = "1.0.6"
1175−source = "registry+https://github.com/rust-lang/crates.io-index"
1176−checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
1177−dependencies = [
1178− "winapi-util",
1179−]
1180−
1181−[[package]]
1182−name = "serde"
1183−version = "1.0.228"
1184−source = "registry+https://github.com/rust-lang/crates.io-index"
1185−checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
1186−dependencies = [
1187− "serde_core",
1188− "serde_derive",
1189−]
1190−
1191−[[package]]
1192−name = "serde-generate"
1193−version = "0.26.0"
1194−source = "registry+https://github.com/rust-lang/crates.io-index"
1195−checksum = "8309fc8d475cf5884e92a463b3a003d433684335be19c3f739af0451b027254b"
1196−dependencies = [
1197− "heck 0.3.3",
1198− "include_dir 0.6.2",
1199− "phf",
1200− "serde",
1201− "serde-reflection",
1202− "textwrap 0.13.4",
1203−]
1204−
1205−[[package]]
1206−name = "serde-reflection"
1207−version = "0.4.0"
1208−source = "registry+https://github.com/rust-lang/crates.io-index"
1209−checksum = "5b6798a64289ff550d8d79847467789a5fd30b42c9c406a4d6dc0bc9b567e55c"
1210−dependencies = [
1211− "once_cell",
1212− "serde",
1213− "thiserror 1.0.69",
1214−]
1215−
1216−[[package]]
1217−name = "serde_bytes"
1218−version = "0.11.19"
1219−source = "registry+https://github.com/rust-lang/crates.io-index"
1220−checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8"
1221−dependencies = [
1222− "serde",
1223− "serde_core",
1224−]
1225−
1226−[[package]]
1227−name = "serde_core"
1228−version = "1.0.228"
1229−source = "registry+https://github.com/rust-lang/crates.io-index"
1230−checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
1231−dependencies = [
1232− "serde_derive",
1233−]
1234−
1235−[[package]]
1236−name = "serde_derive"
1237−version = "1.0.228"
1238−source = "registry+https://github.com/rust-lang/crates.io-index"
1239−checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
1240−dependencies = [
1241− "proc-macro2",
1242− "quote",
1243− "syn 2.0.118",
1244−]
1245−
1246−[[package]]
1247−name = "serde_json"
1248−version = "1.0.150"
1249−source = "registry+https://github.com/rust-lang/crates.io-index"
1250−checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
1251−dependencies = [
1252− "itoa",
1253− "memchr",
1254− "serde",
1255− "serde_core",
1256− "zmij",
1257−]
1258−
1259−[[package]]
1260−name = "serde_qs"
1261−version = "1.1.2"
1262−source = "registry+https://github.com/rust-lang/crates.io-index"
1263−checksum = "67d525c8ff68aa99e5818302259bdd02d86d0303710616f39c0f44846ff6d332"
1264−dependencies = [
1265− "itoa",
1266− "percent-encoding",
1267− "ryu",
1268− "serde",
1269−]
1270−
1271−[[package]]
1272−name = "shared"
1273−version = "0.1.0"
1274−dependencies = [
1275− "anyhow",
1276− "boltffi",
1277− "clap",
1278− "crux_core",
1279− "crux_http",
1280− "facet",
1281− "serde",
1282−]
1283−
1284−[[package]]
1285−name = "siphasher"
1286−version = "0.3.11"
1287−source = "registry+https://github.com/rust-lang/crates.io-index"
1288−checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
1289−
1290−[[package]]
1291−name = "slab"
1292−version = "0.4.12"
1293−source = "registry+https://github.com/rust-lang/crates.io-index"
1294−checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
1295−
1296−[[package]]
1297−name = "smallvec"
1298−version = "1.15.2"
1299−source = "registry+https://github.com/rust-lang/crates.io-index"
1300−checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
1301−
1302−[[package]]
1303−name = "smawk"
1304−version = "0.3.3"
1305−source = "registry+https://github.com/rust-lang/crates.io-index"
1306−checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100"
1307−
1308−[[package]]
1309−name = "stable_deref_trait"
1310−version = "1.2.1"
1311−source = "registry+https://github.com/rust-lang/crates.io-index"
1312−checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
1313−
1314−[[package]]
1315−name = "strsim"
1316−version = "0.11.1"
1317−source = "registry+https://github.com/rust-lang/crates.io-index"
1318−checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
1319−
1320−[[package]]
1321−name = "syn"
1322−version = "1.0.109"
1323−source = "registry+https://github.com/rust-lang/crates.io-index"
1324−checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
1325−dependencies = [
1326− "proc-macro2",
1327− "quote",
1328− "unicode-ident",
1329−]
1330−
1331−[[package]]
1332−name = "syn"
1333−version = "2.0.118"
1334−source = "registry+https://github.com/rust-lang/crates.io-index"
1335−checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
1336−dependencies = [
1337− "proc-macro2",
1338− "quote",
1339− "unicode-ident",
1340−]
1341−
1342−[[package]]
1343−name = "synstructure"
1344−version = "0.13.2"
1345−source = "registry+https://github.com/rust-lang/crates.io-index"
1346−checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
1347−dependencies = [
1348− "proc-macro2",
1349− "quote",
1350− "syn 2.0.118",
1351−]
1352−
1353−[[package]]
1354−name = "textwrap"
1355−version = "0.13.4"
1356−source = "registry+https://github.com/rust-lang/crates.io-index"
1357−checksum = "cd05616119e612a8041ef58f2b578906cc2531a6069047ae092cfb86a325d835"
1358−dependencies = [
1359− "smawk",
1360− "unicode-width 0.1.14",
1361−]
1362−
1363−[[package]]
1364−name = "textwrap"
1365−version = "0.16.2"
1366−source = "registry+https://github.com/rust-lang/crates.io-index"
1367−checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
1368−dependencies = [
1369− "smawk",
1370− "unicode-linebreak",
1371− "unicode-width 0.2.2",
1372−]
1373−
1374−[[package]]
1375−name = "thiserror"
1376−version = "1.0.69"
1377−source = "registry+https://github.com/rust-lang/crates.io-index"
1378−checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
1379−dependencies = [
1380− "thiserror-impl 1.0.69",
1381−]
1382−
1383−[[package]]
1384−name = "thiserror"
1385−version = "2.0.18"
1386−source = "registry+https://github.com/rust-lang/crates.io-index"
1387−checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
1388−dependencies = [
1389− "thiserror-impl 2.0.18",
1390−]
1391−
1392−[[package]]
1393−name = "thiserror-impl"
1394−version = "1.0.69"
1395−source = "registry+https://github.com/rust-lang/crates.io-index"
1396−checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
1397−dependencies = [
1398− "proc-macro2",
1399− "quote",
1400− "syn 2.0.118",
1401−]
1402−
1403−[[package]]
1404−name = "thiserror-impl"
1405−version = "2.0.18"
1406−source = "registry+https://github.com/rust-lang/crates.io-index"
1407−checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
1408−dependencies = [
1409− "proc-macro2",
1410− "quote",
1411− "syn 2.0.118",
1412−]
1413−
1414−[[package]]
1415−name = "tinystr"
1416−version = "0.8.3"
1417−source = "registry+https://github.com/rust-lang/crates.io-index"
1418−checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
1419−dependencies = [
1420− "displaydoc",
1421− "zerovec",
1422−]
1423−
1424−[[package]]
1425−name = "unicode-ident"
1426−version = "1.0.24"
1427−source = "registry+https://github.com/rust-lang/crates.io-index"
1428−checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
1429−
1430−[[package]]
1431−name = "unicode-linebreak"
1432−version = "0.1.5"
1433−source = "registry+https://github.com/rust-lang/crates.io-index"
1434−checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
1435−
1436−[[package]]
1437−name = "unicode-segmentation"
1438−version = "1.13.3"
1439−source = "registry+https://github.com/rust-lang/crates.io-index"
1440−checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
1441−
1442−[[package]]
1443−name = "unicode-width"
1444−version = "0.1.14"
1445−source = "registry+https://github.com/rust-lang/crates.io-index"
1446−checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
1447−
1448−[[package]]
1449−name = "unicode-width"
1450−version = "0.2.2"
1451−source = "registry+https://github.com/rust-lang/crates.io-index"
1452−checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
1453−
1454−[[package]]
1455−name = "unsynn"
1456−version = "0.3.0"
1457−source = "registry+https://github.com/rust-lang/crates.io-index"
1458−checksum = "501a7adf1a4bd9951501e5c66621e972ef8874d787628b7f90e64f936ef7ec0a"
1459−dependencies = [
1460− "mutants",
1461− "proc-macro2",
1462− "rustc-hash",
1463−]
1464−
1465−[[package]]
1466−name = "url"
1467−version = "2.5.8"
1468−source = "registry+https://github.com/rust-lang/crates.io-index"
1469−checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
1470−dependencies = [
1471− "form_urlencoded",
1472− "idna",
1473− "percent-encoding",
1474− "serde",
1475−]
1476−
1477−[[package]]
1478−name = "utf8_iter"
1479−version = "1.0.4"
1480−source = "registry+https://github.com/rust-lang/crates.io-index"
1481−checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
1482−
1483−[[package]]
1484−name = "utf8parse"
1485−version = "0.2.2"
1486−source = "registry+https://github.com/rust-lang/crates.io-index"
1487−checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
1488−
1489−[[package]]
1490−name = "uuid"
1491−version = "1.23.4"
1492−source = "registry+https://github.com/rust-lang/crates.io-index"
1493−checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53"
1494−dependencies = [
1495− "js-sys",
1496− "wasm-bindgen",
1497−]
1498−
1499−[[package]]
1500−name = "version_check"
1501−version = "0.9.5"
1502−source = "registry+https://github.com/rust-lang/crates.io-index"
1503−checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
1504−
1505−[[package]]
1506−name = "walkdir"
1507−version = "2.5.0"
1508−source = "registry+https://github.com/rust-lang/crates.io-index"
1509−checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
1510−dependencies = [
1511− "same-file",
1512− "winapi-util",
1513−]
1514−
1515−[[package]]
1516−name = "wasi"
1517−version = "0.11.1+wasi-snapshot-preview1"
1518−source = "registry+https://github.com/rust-lang/crates.io-index"
1519−checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
1520−
1521−[[package]]
1522−name = "wasm-bindgen"
1523−version = "0.2.126"
1524−source = "registry+https://github.com/rust-lang/crates.io-index"
1525−checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
1526−dependencies = [
1527− "cfg-if",
1528− "once_cell",
1529− "rustversion",
1530− "wasm-bindgen-macro",
1531− "wasm-bindgen-shared",
1532−]
1533−
1534−[[package]]
1535−name = "wasm-bindgen-macro"
1536−version = "0.2.126"
1537−source = "registry+https://github.com/rust-lang/crates.io-index"
1538−checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
1539−dependencies = [
1540− "quote",
1541− "wasm-bindgen-macro-support",
1542−]
1543−
1544−[[package]]
1545−name = "wasm-bindgen-macro-support"
1546−version = "0.2.126"
1547−source = "registry+https://github.com/rust-lang/crates.io-index"
1548−checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
1549−dependencies = [
1550− "bumpalo",
1551− "proc-macro2",
1552− "quote",
1553− "syn 2.0.118",
1554− "wasm-bindgen-shared",
1555−]
1556−
1557−[[package]]
1558−name = "wasm-bindgen-shared"
1559−version = "0.2.126"
1560−source = "registry+https://github.com/rust-lang/crates.io-index"
1561−checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
1562−dependencies = [
1563− "unicode-ident",
1564−]
1565−
1566−[[package]]
1567−name = "web-sys"
1568−version = "0.3.103"
1569−source = "registry+https://github.com/rust-lang/crates.io-index"
1570−checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141"
1571−dependencies = [
1572− "js-sys",
1573− "wasm-bindgen",
1574−]
1575−
1576−[[package]]
1577−name = "winapi-util"
1578−version = "0.1.11"
1579−source = "registry+https://github.com/rust-lang/crates.io-index"
1580−checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
1581−dependencies = [
1582− "windows-sys",
1583−]
1584−
1585−[[package]]
1586−name = "windows-link"
1587−version = "0.2.1"
1588−source = "registry+https://github.com/rust-lang/crates.io-index"
1589−checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
1590−
1591−[[package]]
1592−name = "windows-sys"
1593−version = "0.61.2"
1594−source = "registry+https://github.com/rust-lang/crates.io-index"
1595−checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
1596−dependencies = [
1597− "windows-link",
1598−]
1599−
1600−[[package]]
1601−name = "writeable"
1602−version = "0.6.3"
1603−source = "registry+https://github.com/rust-lang/crates.io-index"
1604−checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
1605−
1606−[[package]]
1607−name = "yoke"
1608−version = "0.8.3"
1609−source = "registry+https://github.com/rust-lang/crates.io-index"
1610−checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
1611−dependencies = [
1612− "stable_deref_trait",
1613− "yoke-derive",
1614− "zerofrom",
1615−]
1616−
1617−[[package]]
1618−name = "yoke-derive"
1619−version = "0.8.2"
1620−source = "registry+https://github.com/rust-lang/crates.io-index"
1621−checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
1622−dependencies = [
1623− "proc-macro2",
1624− "quote",
1625− "syn 2.0.118",
1626− "synstructure",
1627−]
1628−
1629−[[package]]
1630−name = "zerocopy"
1631−version = "0.8.53"
1632−source = "registry+https://github.com/rust-lang/crates.io-index"
1633−checksum = "75726053136156d419e285b9b7eddaaea9e3fea6ce32eed44a89901f0bd98de1"
1634−dependencies = [
1635− "zerocopy-derive",
1636−]
1637−
1638−[[package]]
1639−name = "zerocopy-derive"
1640−version = "0.8.53"
1641−source = "registry+https://github.com/rust-lang/crates.io-index"
1642−checksum = "4714fd92cf900833d49538023a9b3915155210801d1c1169eba513b2addefd71"
1643−dependencies = [
1644− "proc-macro2",
1645− "quote",
1646− "syn 2.0.118",
1647−]
1648−
1649−[[package]]
1650−name = "zerofrom"
1651−version = "0.1.8"
1652−source = "registry+https://github.com/rust-lang/crates.io-index"
1653−checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
1654−dependencies = [
1655− "zerofrom-derive",
1656−]
1657−
1658−[[package]]
1659−name = "zerofrom-derive"
1660−version = "0.1.7"
1661−source = "registry+https://github.com/rust-lang/crates.io-index"
1662−checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
1663−dependencies = [
1664− "proc-macro2",
1665− "quote",
1666− "syn 2.0.118",
1667− "synstructure",
1668−]
1669−
1670−[[package]]
1671−name = "zerotrie"
1672−version = "0.2.4"
1673−source = "registry+https://github.com/rust-lang/crates.io-index"
1674−checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
1675−dependencies = [
1676− "displaydoc",
1677− "yoke",
1678− "zerofrom",
1679−]
1680−
1681−[[package]]
1682−name = "zerovec"
1683−version = "0.11.6"
1684−source = "registry+https://github.com/rust-lang/crates.io-index"
1685−checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
1686−dependencies = [
1687− "yoke",
1688− "zerofrom",
1689− "zerovec-derive",
1690−]
1691−
1692−[[package]]
1693−name = "zerovec-derive"
1694−version = "0.11.3"
1695−source = "registry+https://github.com/rust-lang/crates.io-index"
1696−checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
1697−dependencies = [
1698− "proc-macro2",
1699− "quote",
1700− "syn 2.0.118",
1701−]
1702−
1703−[[package]]
1704−name = "zmij"
1705−version = "1.0.21"
1706−source = "registry+https://github.com/rust-lang/crates.io-index"
1707−checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
shared/Cargo.tomldeleted−31
@@ -1,31 +0,0 @@
1−[package]
2−name = "shared"
3−version = "0.1.0"
4−edition = "2024"
5−rust-version = "1.90"
6−license = "Apache-2.0"
7−
8−[lib]
9−crate-type = ["cdylib", "lib", "staticlib"]
10−
11−[[bin]]
12−name = "codegen"
13−required-features = ["codegen"]
14−
15−[features]
16−facet_typegen = ["crux_core/facet_typegen"]
17−codegen = ["facet_typegen", "dep:clap", "dep:anyhow", "crux_core/typegen"]
18−
19−[dependencies]
20−facet = "=0.44"
21−boltffi = "0.27"
22−crux_core = "0.19"
23−crux_http = "0.19"
24−serde = { version = "1", features = ["derive"] }
25−
26−# Optional codegen deps
27−anyhow = { version = "1", optional = true }
28−clap = { version = "4", optional = true, features = ["derive"] }
29−
30−[dev-dependencies]
31−crux_core = { version = "0.19", features = ["testing"] }
shared/build.gradle.ktsdeleted−39
@@ -1,39 +0,0 @@
1−import org.jetbrains.kotlin.gradle.dsl.JvmTarget
2−
3−plugins {
4− alias(libs.plugins.android.library)
5− alias(libs.plugins.kotlin.android)
6−}
7−
8−android {
9− namespace = "soy.iko.opencode.core"
10−
11− compileSdk = 37
12− buildToolsVersion = "37.0.0"
13−
14− defaultConfig {
15− minSdk = 26
16− }
17−
18− compileOptions {
19− sourceCompatibility = JavaVersion.VERSION_11
20− targetCompatibility = JavaVersion.VERSION_11
21− }
22−
23− kotlin {
24− compilerOptions {
25− jvmTarget = JvmTarget.JVM_11
26− }
27− }
28−
29− sourceSets {
30− getByName("main") {
31− kotlin.srcDirs("${projectDir}/../generated")
32− jniLibs.srcDirs("${projectDir}/../generated/jniLibs")
33− }
34− }
35−}
36−
37−dependencies {
38− implementation(libs.kotlinx.coroutines.android)
39−}
shared/src/app.rsdeleted−494
@@ -1,494 +0,0 @@
1−use crux_core::{
2− App, Command,
3− macros::effect,
4− render::{RenderOperation, render},
5−};
6−use crux_http::protocol::HttpRequest;
7−use facet::Facet;
8−use serde::{Deserialize, Serialize};
9−
10−// ─── Events ────────────────────────────────────────────────────────────────
11−
12−#[derive(Facet, Serialize, Deserialize, Clone, Debug)]
13−#[repr(C)]
14−pub enum Event {
15− // Lifecycle
16− Start,
17− ServerUrlChanged(String),
18− Connect,
19−
20− // Session list
21− LoadSessions,
22− // events local to the core
23− #[serde(skip)]
24− #[facet(skip)]
25− SessionsLoaded(#[facet(opaque)] HttpResult<String>),
26− SelectSession(String),
27− CreateSession,
28− SessionCreated(#[facet(opaque)] HttpResult<String>),
29−
30− // Chat
31− LoadMessages(String),
32− MessagesLoaded(#[facet(opaque)] HttpResult<String>),
33− SendMessage(String),
34− MessageSent(#[facet(opaque)] HttpResult<String>),
35−
36− // SSE event received
37− EventReceived(String),
38−
39− // Navigation
40− NavigateToChat(String),
41− NavigateToSessions,
42−
43− // Errors
44− DismissError,
45−}
46−
47−// ─── Effects ───────────────────────────────────────────────────────────────
48−
49−#[effect(facet_typegen)]
50−#[derive(Debug)]
51−pub enum Effect {
52− Render(RenderOperation),
53− Http(HttpRequest),
54−}
55−
56−// ─── HTTP result wrapper (opaque, not type-generated) ──────────────────────
57−
58−pub type HttpResult<T> = Result<crux_http::Response<T>, crux_http::HttpError>;
59−
60−// ─── Model ─────────────────────────────────────────────────────────────────
61−
62−#[derive(Default)]
63−pub struct Model {
64− server_url: String,
65− connected: bool,
66− loading: bool,
67− error: Option<String>,
68− sessions: Vec<SessionSummary>,
69− current_session_id: Option<String>,
70− messages: Vec<MessageView>,
71− draft_message: String,
72−}
73−
74−#[derive(Clone, Debug, Default)]
75−pub struct SessionSummary {
76− pub id: String,
77− pub title: String,
78−}
79−
80−#[derive(Facet, Serialize, Deserialize, Clone, Debug, Default)]
81−pub struct MessageView {
82− pub id: String,
83− pub role: String,
84− pub text: String,
85− pub time: u64,
86−}
87−
88−// ─── ViewModel ─────────────────────────────────────────────────────────────
89−
90−#[derive(Facet, Serialize, Deserialize, Clone, Debug, Default)]
91−pub struct ViewModel {
92− pub screen: Screen,
93− pub server_url: String,
94− pub connected: bool,
95− pub loading: bool,
96− pub error: Option<String>,
97− pub sessions: Vec<SessionView>,
98− pub current_session_id: Option<String>,
99− pub messages: Vec<MessageView>,
100− pub draft_message: String,
101−}
102−
103−#[derive(Facet, Serialize, Deserialize, Clone, Debug, Default, PartialEq, Eq)]
104−#[repr(C)]
105−pub enum Screen {
106− #[default]
107− Connect,
108− Sessions,
109− Chat,
110−}
111−
112−#[derive(Facet, Serialize, Deserialize, Clone, Debug, Default)]
113−pub struct SessionView {
114− pub id: String,
115− pub title: String,
116−}
117−
118−// ─── App ───────────────────────────────────────────────────────────────────
119−
120−#[derive(Default)]
121−pub struct OpencodeApp;
122−
123−impl App for OpencodeApp {
124− type Event = Event;
125− type Model = Model;
126− type ViewModel = ViewModel;
127− type Effect = Effect;
128−
129− fn update(&self, event: Event, model: &mut Model) -> Command<Effect, Event> {
130− match event {
131− Event::Start => {
132− model.server_url = "http://localhost:4096".to_string();
133− render()
134− }
135−
136− Event::ServerUrlChanged(url) => {
137− model.server_url = url;
138− render()
139− }
140−
141− Event::Connect => {
142− model.loading = true;
143− model.error = None;
144− let url = format!("{}/global/health", model.server_url);
145− Http::get(&url).expect_string().build().then_send(|_| Event::LoadSessions)
146− }
147−
148− Event::LoadSessions => {
149− model.loading = true;
150− let url = format!("{}/session", model.server_url);
151− Http::get(&url).expect_string().build().then_send(Event::SessionsLoaded)
152− }
153−
154− Event::SessionsLoaded(result) => {
155− model.loading = false;
156− match result {
157− Ok(mut response) => {
158− let body = response.take_body().unwrap_or_default();
159− model.sessions = parse_sessions(&body);
160− model.connected = true;
161− render()
162− }
163− Err(e) => {
164− model.error = Some(format!("Failed to load sessions: {e}"));
165− render()
166− }
167− }
168− }
169−
170− Event::SelectSession(id) => {
171− model.current_session_id = Some(id.clone());
172− Command::event(Event::NavigateToChat(id))
173− }
174−
175− Event::NavigateToChat(id) => {
176− model.current_session_id = Some(id.clone());
177− model.messages.clear();
178− render().and(Command::event(Event::LoadMessages(id)))
179− }
180−
181− Event::NavigateToSessions => {
182− model.current_session_id = None;
183− model.messages.clear();
184− render()
185− }
186−
187− Event::CreateSession => {
188− model.loading = true;
189− let url = format!("{}/session", model.server_url);
190− Http::post(&url).expect_string().build().then_send(Event::SessionCreated)
191− }
192−
193− Event::SessionCreated(result) => {
194− model.loading = false;
195− match result {
196− Ok(mut response) => {
197− let body = response.take_body().unwrap_or_default();
198− let id = parse_session_id(&body);
199− Command::event(Event::NavigateToChat(id))
200− }
201− Err(e) => {
202− model.error = Some(format!("Failed to create session: {e}"));
203− render()
204− }
205− }
206− }
207−
208− Event::LoadMessages(session_id) => {
209− model.loading = true;
210− let url = format!("{}/session/{}/message", model.server_url, session_id);
211− Http::get(&url).expect_string().build().then_send(Event::MessagesLoaded)
212− }
213−
214− Event::MessagesLoaded(result) => {
215− model.loading = false;
216− match result {
217− Ok(mut response) => {
218− let body = response.take_body().unwrap_or_default();
219− model.messages = parse_messages(&body);
220− render()
221− }
222− Err(e) => {
223− model.error = Some(format!("Failed to load messages: {e}"));
224− render()
225− }
226− }
227− }
228−
229− Event::SendMessage(text) => {
230− let session_id = match &model.current_session_id {
231− Some(id) => id.clone(),
232− None => return render(),
233− };
234− model.draft_message.clear();
235− let url = format!(
236− "{}/session/{}/prompt_async",
237− model.server_url, session_id
238− );
239− let body = format!(
240− r#"{{"sessionID":"{}","parts":[{{"type":"text","text":"{}"}}]}}"#,
241− session_id, text
242− );
243− Http::post(&url)
244− .header("Content-Type", "application/json")
245− .body_bytes(body.into_bytes())
246− .expect_string()
247− .build()
248− .then_send(Event::MessageSent)
249− }
250−
251− Event::MessageSent(result) => {
252− match result {
253− Ok(_) => {
254− // Reload messages to get the response
255− match &model.current_session_id {
256− Some(id) => Command::event(Event::LoadMessages(id.clone())),
257− None => render(),
258− }
259− }
260− Err(e) => {
261− model.error = Some(format!("Failed to send message: {e}"));
262− render()
263− }
264− }
265− }
266−
267− Event::EventReceived(_data) => {
268− // SSE event — reload messages to show updates
269− match &model.current_session_id {
270− Some(id) => Command::event(Event::LoadMessages(id.clone())),
271− None => render(),
272− }
273− }
274−
275− Event::DismissError => {
276− model.error = None;
277− render()
278− }
279− }
280− }
281−
282− fn view(&self, model: &Model) -> ViewModel {
283− ViewModel {
284− screen: if model.current_session_id.is_some() {
285− Screen::Chat
286− } else if model.connected {
287− Screen::Sessions
288− } else {
289− Screen::Connect
290− },
291− server_url: model.server_url.clone(),
292− connected: model.connected,
293− loading: model.loading,
294− error: model.error.clone(),
295− sessions: model
296− .sessions
297− .iter()
298− .map(|s| SessionView {
299− id: s.id.clone(),
300− title: s.title.clone(),
301− })
302− .collect(),
303− current_session_id: model.current_session_id.clone(),
304− messages: model.messages.clone(),
305− draft_message: model.draft_message.clone(),
306− }
307− }
308−}
309−
310−// ─── JSON parsing helpers (naive — the core stays pure) ────────────────────
311−
312−fn parse_sessions(json: &str) -> Vec<SessionSummary> {
313− let mut sessions = Vec::new();
314− let mut depth = 0;
315− let mut start = 0;
316− let bytes = json.as_bytes();
317−
318− let mut i = 0;
319− while i < bytes.len() {
320− match bytes[i] {
321− b'{' if depth == 0 => {
322− depth = 1;
323− start = i;
324− }
325− b'{' => depth += 1,
326− b'}' => {
327− depth -= 1;
328− if depth == 0 {
329− let obj = &json[start..=i];
330− let id = extract_string(obj, "id").unwrap_or_default();
331− let title = extract_string(obj, "title").unwrap_or_default();
332− if !id.is_empty() {
333− sessions.push(SessionSummary { id, title });
334− }
335− }
336− }
337− _ => {}
338− }
339− i += 1;
340− }
341− sessions
342−}
343−
344−fn parse_session_id(json: &str) -> String {
345− extract_string(json, "id").unwrap_or_default()
346−}
347−
348−fn parse_messages(json: &str) -> Vec<MessageView> {
349− let mut messages = Vec::new();
350− let mut depth = 0;
351− let mut start = 0;
352− let bytes = json.as_bytes();
353−
354− let mut i = 0;
355− while i < bytes.len() {
356− match bytes[i] {
357− b'{' if depth == 0 => {
358− depth = 1;
359− start = i;
360− }
361− b'{' => depth += 1,
362− b'}' => {
363− depth -= 1;
364− if depth == 0 {
365− let obj = &json[start..=i];
366− let id = extract_string(obj, "id").unwrap_or_default();
367− let role = extract_string(obj, "role").unwrap_or_default();
368− let time = extract_number(obj, "created").unwrap_or(0);
369− if !id.is_empty() && !role.is_empty() {
370− messages.push(MessageView {
371− id,
372− role,
373− text: String::new(),
374− time,
375− });
376− }
377− }
378− }
379− _ => {}
380− }
381− i += 1;
382− }
383− messages
384−}
385−
386−fn extract_string(json: &str, key: &str) -> Option<String> {
387− let pattern = format!("\"{key}\"");
388− let idx = json.find(&pattern)?;
389− let rest = &json[idx + pattern.len()..];
390− let colon = rest.find(':')?;
391− let rest = &rest[colon + 1..];
392− let quote = rest.find('"')?;
393− let rest = &rest[quote + 1..];
394− let end = rest.find('"')?;
395− Some(rest[..end].to_string())
396−}
397−
398−fn extract_number(json: &str, key: &str) -> Option<u64> {
399− let pattern = format!("\"{key}\"");
400− let idx = json.find(&pattern)?;
401− let rest = &json[idx + pattern.len()..];
402− let colon = rest.find(':')?;
403− let rest = rest[colon + 1..].trim_start();
404− let end = rest
405− .find(|c: char| !c.is_ascii_digit())
406− .unwrap_or(rest.len());
407− rest[..end].parse().ok()
408−}
409−
410−// ─── HTTP type alias for the command builder ───────────────────────────────
411−
412−use crux_http::command::Http;
413−
414−#[cfg(test)]
415−mod test {
416− use super::*;
417−
418− #[test]
419− fn start_sets_default_server_url() {
420− let app = OpencodeApp;
421− let mut model = Model::default();
422− app.update(Event::Start, &mut model).expect_only_render();
423− assert_eq!(model.server_url, "http://localhost:4096");
424− }
425−
426− #[test]
427− fn server_url_change_updates_model() {
428− let app = OpencodeApp;
429− let mut model = Model::default();
430− app.update(Event::Start, &mut model);
431− app.update(
432− Event::ServerUrlChanged("http://10.0.0.1:4096".to_string()),
433− &mut model,
434− )
435− .expect_only_render();
436− assert_eq!(model.server_url, "http://10.0.0.1:4096");
437− }
438−
439− #[test]
440− fn connect_emits_http_effect() {
441− let app = OpencodeApp;
442− let mut model = Model::default();
443− app.update(Event::Start, &mut model);
444− let mut cmd = app.update(Event::Connect, &mut model);
445− assert!(cmd.effects().any(|e| matches!(e, Effect::Http(_))));
446− assert!(model.loading);
447− }
448−
449− #[test]
450− fn parse_sessions_extracts_id_and_title() {
451− let json = r#"[{"id":"abc123","title":"My Session"},{"id":"def456","title":"Test"}]"#;
452− let sessions = parse_sessions(json);
453− assert_eq!(sessions.len(), 2);
454− assert_eq!(sessions[0].id, "abc123");
455− assert_eq!(sessions[0].title, "My Session");
456− assert_eq!(sessions[1].id, "def456");
457− }
458−
459− #[test]
460− fn parse_session_id_extracts_id() {
461− let json = r#"{"id":"xyz789","title":"New"}"#;
462− let id = parse_session_id(json);
463− assert_eq!(id, "xyz789");
464− }
465−
466− #[test]
467− fn extract_string_finds_nested_keys() {
468− let json = r#"{"a":"b","id":"target","c":"d"}"#;
469− assert_eq!(extract_string(json, "id"), Some("target".to_string()));
470− assert_eq!(extract_string(json, "missing"), None);
471− }
472−
473− #[test]
474− fn extract_number_parses_values() {
475− let json = r#"{"created":1234567890}"#;
476− assert_eq!(extract_number(json, "created"), Some(1234567890));
477− }
478−
479− #[test]
480− fn navigate_to_sessions_clears_state() {
481− let app = OpencodeApp;
482− let mut model = Model::default();
483− model.current_session_id = Some("test".to_string());
484− model.messages.push(MessageView {
485− id: "m1".to_string(),
486− role: "user".to_string(),
487− text: "hello".to_string(),
488− time: 0,
489− });
490− app.update(Event::NavigateToSessions, &mut model);
491− assert!(model.current_session_id.is_none());
492− assert!(model.messages.is_empty());
493− }
494−}
shared/src/bin/codegen.rsdeleted−40
@@ -1,40 +0,0 @@
1−use std::path::PathBuf;
2−
3−use clap::{Parser, ValueEnum};
4−use crux_core::type_generation::facet::{Config, TypeRegistry};
5−use shared::OpencodeApp;
6−
7−#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
8−enum Language {
9− Kotlin,
10−}
11−
12−#[derive(Parser)]
13−#[command(version, about, long_about = None)]
14−struct Args {
15− #[arg(short, long, value_enum)]
16− language: Language,
17− #[arg(short, long)]
18− output_dir: PathBuf,
19−}
20−
21−fn main() -> anyhow::Result<()> {
22− let args = Args::parse();
23−
24− let typegen_app = TypeRegistry::new()
25− .register_app::<OpencodeApp>()?
26− .build()?;
27−
28− let name = match args.language {
29− Language::Kotlin => "soy.iko.opencode.core",
30− };
31− let config = Config::builder(name, &args.output_dir).build();
32−
33− match args.language {
34− Language::Kotlin => {
35− typegen_app.kotlin(&config)?;
36− }
37− }
38−
39− Ok(())
40−}
shared/src/ffi.rsdeleted−53
@@ -1,53 +0,0 @@
1−use crux_core::{
2− Core,
3− bridge::{Bridge, EffectId},
4−};
5−
6−use crate::OpencodeApp;
7−
8−pub struct CoreFfi {
9− core: Bridge<OpencodeApp>,
10−}
11−
12−impl Default for CoreFfi {
13− fn default() -> Self {
14− Self::new()
15− }
16−}
17−
18−#[boltffi::export]
19−impl CoreFfi {
20− #[must_use]
21− pub fn new() -> Self {
22− Self {
23− core: Bridge::new(Core::new()),
24− }
25− }
26−
27− #[must_use]
28− pub fn update(&self, data: &[u8]) -> Vec<u8> {
29− let mut effects = Vec::new();
30− match self.core.update(data, &mut effects) {
31− Ok(()) => effects,
32− Err(e) => panic!("{e}"),
33− }
34− }
35−
36− #[must_use]
37− pub fn resolve(&self, id: u32, data: &[u8]) -> Vec<u8> {
38− let mut effects = Vec::new();
39− match self.core.resolve(EffectId(id), data, &mut effects) {
40− Ok(()) => effects,
41− Err(e) => panic!("{e}"),
42− }
43− }
44−
45− #[must_use]
46− pub fn view(&self) -> Vec<u8> {
47− let mut view_model = Vec::new();
48− match self.core.view(&mut view_model) {
49− Ok(()) => view_model,
50− Err(e) => panic!("{e}"),
51− }
52− }
53−}
shared/src/lib.rsdeleted−6
@@ -1,6 +0,0 @@
1−#![allow(clippy::used_underscore_items)]
2−
3−mod app;
4−pub mod ffi;
5−
6−pub use app::*;