diff --git a/apps/android/app/build.gradle.kts b/apps/android/app/build.gradle.kts
index 229cc171..34e03b75 100644
--- a/apps/android/app/build.gradle.kts
+++ b/apps/android/app/build.gradle.kts
@@ -136,33 +136,16 @@ dependencies {
// Chrome/composer icons (mic, attach, pair, settings, menu, close). The
// full extended set is large, but release builds R8-minify (above) and
// drop every unused icon; debug builds simply carry the dead weight.
- implementation("androidx.compose.material:material-icons-extended")
+ implementation(libs.androidx.material.icons.extended)
implementation(libs.kotlinx.coroutines.android)
- // JNA: the FFI bridge uniffi-bindgen's generated Kotlin uses to call into
- // crates/client-ffi's cdylib. Version pinned loosely on purpose — this
- // is exactly the dependency the generated bindings file itself declares
- // as a prerequisite; keep it in step with whatever `uniffi` crate version
- // crates/client-ffi/Cargo.toml pins (currently 0.32).
- implementation("net.java.dev.jna:jna:5.19.0@aar")
-
- // Tink directly, not its deprecated androidx.security:security-crypto
- // wrapper (frozen at 1.1.0-alpha07): platform/SecureIdentityStore.kt
- // uses AndroidKeysetManager to keep the persisted bridge identity
- // secret encrypted at rest under a Keystore-held master key.
- implementation("com.google.crypto.tink:tink-android:1.19.0")
-
- // Pairing-QR camera scan (ui/screens/PairingScanView.kt): CameraX for the
- // preview/analysis pipeline, ZXing's core decoder for the QR code — a
- // plain Java library with no dependencies of its own. Not ML Kit: it
- // brings Play Services, Firebase components and a usage-reporting
- // transport (a startup provider, a background upload job) into every
- // launch, all to read one QR code. Neither library is covered by the
- // Compose BOM, so they are pinned explicitly.
- val camerax = "1.6.2"
- implementation("androidx.camera:camera-camera2:$camerax")
- implementation("androidx.camera:camera-lifecycle:$camerax")
- implementation("androidx.camera:camera-view:$camerax")
- implementation("com.google.zxing:core:3.5.4")
+ // The aar: JNA's Android build, with its native dispatch libraries.
+ // Why each of these is here: gradle/libs.versions.toml.
+ implementation(variantOf(libs.jna) { artifactType("aar") })
+ implementation(libs.tink.android)
+ implementation(libs.androidx.camera.camera2)
+ implementation(libs.androidx.camera.lifecycle)
+ implementation(libs.androidx.camera.view)
+ implementation(libs.zxing.core)
// Compose-native Markdown for assistant/plan transcript rows.
// GFM (tables, task lists, strikethrough, autolinks) is the renderer's
diff --git a/apps/android/app/lint.xml b/apps/android/app/lint.xml
new file mode 100644
index 00000000..62742505
--- /dev/null
+++ b/apps/android/app/lint.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/apps/android/app/src/main/AndroidManifest.xml b/apps/android/app/src/main/AndroidManifest.xml
index 2021a00e..ec2ad8db 100644
--- a/apps/android/app/src/main/AndroidManifest.xml
+++ b/apps/android/app/src/main/AndroidManifest.xml
@@ -28,9 +28,11 @@
-
+
edit.putString(KIND, "device")
- is Login.SignerApp -> edit.putString(KIND, "signer")
- .putString(PACKAGE, login.packageName)
- .putString(PUBKEY, login.pubkeyHex)
+ // Committed synchronously: the login must be on disk before the core
+ // starts, and the service reads it right after.
+ prefs.edit(commit = true) {
+ clear()
+ when (login) {
+ Login.OnDevice -> putString(KIND, "device")
+ is Login.SignerApp -> putString(KIND, "signer")
+ .putString(PACKAGE, login.packageName)
+ .putString(PUBKEY, login.pubkeyHex)
+ }
}
- // Committed synchronously: the service reads it right after.
- edit.commit()
}
fun clear() {
- prefs.edit().clear().commit()
+ prefs.edit(commit = true) { clear() }
}
}
diff --git a/apps/android/app/src/main/java/com/codedeck/plus/platform/Nip55Signer.kt b/apps/android/app/src/main/java/com/codedeck/plus/platform/Nip55Signer.kt
index b81ed81d..a6e10c9b 100644
--- a/apps/android/app/src/main/java/com/codedeck/plus/platform/Nip55Signer.kt
+++ b/apps/android/app/src/main/java/com/codedeck/plus/platform/Nip55Signer.kt
@@ -4,8 +4,8 @@ import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.database.Cursor
-import android.net.Uri
import android.util.Log
+import androidx.core.net.toUri
import kotlinx.serialization.json.addJsonObject
import kotlinx.serialization.json.buildJsonArray
import kotlinx.serialization.json.put
@@ -39,7 +39,7 @@ data class SignerAppInfo(val packageName: String, val label: String)
/** Every installed NIP-55 signer app, by label. */
fun installedSignerApps(context: Context): List {
val pm = context.packageManager
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:"))
+ val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:".toUri())
return pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
.map { SignerAppInfo(it.activityInfo.packageName, it.loadLabel(pm).toString()) }
.distinctBy { it.packageName }
@@ -50,7 +50,7 @@ fun installedSignerApps(context: Context): List {
* everything the core will need so later requests can be answered without
* the user. */
fun getPublicKeyIntent(packageName: String): Intent =
- Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:")).apply {
+ Intent(Intent.ACTION_VIEW, "nostrsigner:".toUri()).apply {
`package` = packageName
putExtra("type", "get_public_key")
putExtra("permissions", signerPermissionsJson())
@@ -157,7 +157,7 @@ class Nip55Signer(
}
private fun resolve(method: String, payload: String, peer: String): ProviderAnswer {
- val uri = Uri.parse("content://$packageName.$method")
+ val uri = "content://$packageName.$method".toUri()
val cursor: Cursor = try {
// NIP-55 passes the arguments in the projection slot.
app.contentResolver.query(uri, arrayOf(payload, peer, currentUser), null, null, null)
@@ -170,7 +170,7 @@ class Nip55Signer(
private fun viaActivity(type: String, payload: String, peer: String?): SignerAnswer {
val id = UUID.randomUUID().toString()
- val intent = Intent(Intent.ACTION_VIEW, Uri.parse("nostrsigner:$payload")).apply {
+ val intent = Intent(Intent.ACTION_VIEW, "nostrsigner:$payload".toUri()).apply {
`package` = packageName
putExtra("type", type)
putExtra("id", id)
diff --git a/apps/android/app/src/main/java/com/codedeck/plus/platform/Notifier.kt b/apps/android/app/src/main/java/com/codedeck/plus/platform/Notifier.kt
index 5b42879a..2e85e5dc 100644
--- a/apps/android/app/src/main/java/com/codedeck/plus/platform/Notifier.kt
+++ b/apps/android/app/src/main/java/com/codedeck/plus/platform/Notifier.kt
@@ -91,7 +91,11 @@ class Notifier(private val context: Context) : UniffiNotifier {
}
val notification = builder.build()
- NotificationManagerCompat.from(context).notify(effectiveTag, idFor(effectiveTag), notification)
+ try {
+ NotificationManagerCompat.from(context).notify(effectiveTag, idFor(effectiveTag), notification)
+ } catch (e: SecurityException) {
+ // Permission revoked since the check above: drop the post.
+ }
}
override fun cancel(tag: String) {
@@ -119,7 +123,11 @@ class Notifier(private val context: Context) : UniffiNotifier {
),
)
.build()
- NotificationManagerCompat.from(context).notify(SIGNER_TAG, idFor(SIGNER_TAG), notification)
+ try {
+ NotificationManagerCompat.from(context).notify(SIGNER_TAG, idFor(SIGNER_TAG), notification)
+ } catch (e: SecurityException) {
+ // Permission revoked since the check above: drop the post.
+ }
}
fun cancelSignerApproval() = cancel(SIGNER_TAG)
diff --git a/apps/android/app/src/main/java/com/codedeck/plus/platform/SecureIdentityStore.kt b/apps/android/app/src/main/java/com/codedeck/plus/platform/SecureIdentityStore.kt
index 852b35c5..80435177 100644
--- a/apps/android/app/src/main/java/com/codedeck/plus/platform/SecureIdentityStore.kt
+++ b/apps/android/app/src/main/java/com/codedeck/plus/platform/SecureIdentityStore.kt
@@ -1,6 +1,7 @@
package com.codedeck.plus.platform
import android.content.Context
+import androidx.core.content.edit
import com.google.crypto.tink.Aead
import com.google.crypto.tink.KeyTemplates
import com.google.crypto.tink.RegistryConfiguration
@@ -143,7 +144,7 @@ class KeyVault(context: Context) {
}
}
app.getSharedPreferences("codedeck_identity_keyset_prefs", Context.MODE_PRIVATE)
- .edit().clear().apply()
+ .edit { clear() }
identityFile.delete()
sessionKeysFile.delete()
buildKeysetHandle()
diff --git a/apps/android/app/src/main/java/com/codedeck/plus/platform/StayConnectedService.kt b/apps/android/app/src/main/java/com/codedeck/plus/platform/StayConnectedService.kt
index 4f75f3a9..87ac1a27 100644
--- a/apps/android/app/src/main/java/com/codedeck/plus/platform/StayConnectedService.kt
+++ b/apps/android/app/src/main/java/com/codedeck/plus/platform/StayConnectedService.kt
@@ -14,8 +14,8 @@ import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
-import android.util.Log
import android.os.SystemClock
+import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.app.ServiceCompat
@@ -419,14 +419,14 @@ class StayConnectedService : Service() {
* and its bridges is exactly that type, and unlike `dataSync` it has no
* daily time budget — Android 15 stops a `dataSync` service after 6 h per
* 24 h, which silently ended "stay connected". Older versions only know
- * `dataSync` (no budget there).
+ * `dataSync` (no budget there), and before Android 10 a foreground
+ * service has no type at all.
*/
- private fun foregroundType(): Int =
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
- ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
- } else {
- ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
- }
+ private fun foregroundType(): Int = when {
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> ServiceInfo.FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING
+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
+ else -> 0
+ }
/** Re-posting under the foreground notification's own id replaces it in
* place; the platform drops the post silently when notifications are
diff --git a/apps/android/app/src/main/java/com/codedeck/plus/ui/components/Kit.kt b/apps/android/app/src/main/java/com/codedeck/plus/ui/components/Kit.kt
index 3f02d766..f9fa95e2 100644
--- a/apps/android/app/src/main/java/com/codedeck/plus/ui/components/Kit.kt
+++ b/apps/android/app/src/main/java/com/codedeck/plus/ui/components/Kit.kt
@@ -155,9 +155,9 @@ fun IconAction(icon: ImageVector, label: String, onClick: () -> Unit, tint: Colo
*/
@Composable
fun Group(
+ modifier: Modifier = Modifier,
title: String? = null,
footer: String? = null,
- modifier: Modifier = Modifier,
rows: @Composable GroupScope.() -> Unit,
) {
Column(modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(Tokens.Space2)) {
diff --git a/apps/android/app/src/main/java/com/codedeck/plus/ui/screens/SettingsScreen.kt b/apps/android/app/src/main/java/com/codedeck/plus/ui/screens/SettingsScreen.kt
index f38b6287..1d68f46f 100644
--- a/apps/android/app/src/main/java/com/codedeck/plus/ui/screens/SettingsScreen.kt
+++ b/apps/android/app/src/main/java/com/codedeck/plus/ui/screens/SettingsScreen.kt
@@ -5,6 +5,7 @@ import android.content.ClipDescription
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
+import android.os.Build
import android.os.PersistableBundle
import android.provider.Settings
import androidx.activity.compose.BackHandler
@@ -29,6 +30,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
@@ -376,7 +378,7 @@ internal fun AppearancePage(view: UniffiSettingsView, dispatch: (UniffiIntent) -
// Local position while dragging; re-keyed on the stored value
// so a change made elsewhere snaps the thumb to it. The intent
// fires on release.
- var position by remember(view.uiScale) { mutableStateOf(view.uiScale.toFloat()) }
+ var position by remember(view.uiScale) { mutableFloatStateOf(view.uiScale.toFloat()) }
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(Tokens.Space2)) {
Text("${Math.round(position * 100)}%", color = Tokens.Text, fontSize = Tokens.TextXl, modifier = Modifier.weight(1f))
QuietButton("Reset", enabled = position != UI_SCALE_DEFAULT, onClick = {
@@ -592,7 +594,11 @@ private fun SecretKeyGroup(revealKey: suspend () -> String?) {
private fun copySensitive(context: Context, text: String) {
val clipboard = context.getSystemService(ClipboardManager::class.java) ?: return
val clip = ClipData.newPlainText("Secret key", text)
- clip.description.extras = PersistableBundle().apply { putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) }
+ // API 33 only: before that there is no clipboard preview to hide the
+ // secret from.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+ clip.description.extras = PersistableBundle().apply { putBoolean(ClipDescription.EXTRA_IS_SENSITIVE, true) }
+ }
clipboard.setPrimaryClip(clip)
}
diff --git a/apps/android/app/src/main/java/com/codedeck/plus/ui/session/Attachment.kt b/apps/android/app/src/main/java/com/codedeck/plus/ui/session/Attachment.kt
index 922dd6e2..8dd6a750 100644
--- a/apps/android/app/src/main/java/com/codedeck/plus/ui/session/Attachment.kt
+++ b/apps/android/app/src/main/java/com/codedeck/plus/ui/session/Attachment.kt
@@ -7,6 +7,7 @@ import android.net.Uri
import android.provider.OpenableColumns
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
+import androidx.core.graphics.scale
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
@@ -163,7 +164,7 @@ private fun downscaledIfHuge(resolver: ContentResolver, picked: PickedFile, file
BitmapFactory.decodeStream(it, null, BitmapFactory.Options().apply { inSampleSize = sampleSize })
} ?: return null
val scaled = if (decoded.width != targetWidth || decoded.height != targetHeight) {
- Bitmap.createScaledBitmap(decoded, targetWidth, targetHeight, true)
+ decoded.scale(targetWidth, targetHeight, true)
} else {
decoded
}
diff --git a/apps/android/app/src/main/java/com/codedeck/plus/ui/session/Composer.kt b/apps/android/app/src/main/java/com/codedeck/plus/ui/session/Composer.kt
index ea888152..84fcb077 100644
--- a/apps/android/app/src/main/java/com/codedeck/plus/ui/session/Composer.kt
+++ b/apps/android/app/src/main/java/com/codedeck/plus/ui/session/Composer.kt
@@ -168,7 +168,7 @@ internal fun Composer(
.size(44.dp)
.clip(CircleShape)
.background(if (canSend || stop) Tokens.Accent else Tokens.SurfaceHover)
- .clickable(enabled = canSend || stop, onClick = if (stop) onStop!! else onSend)
+ .clickable(enabled = canSend || stop, onClick = if (stop) onStop else onSend)
.semantics { contentDescription = if (stop) "Stop" else "Send" },
contentAlignment = Alignment.Center,
) {
diff --git a/apps/android/app/src/main/java/com/codedeck/plus/ui/session/SessionScreen.kt b/apps/android/app/src/main/java/com/codedeck/plus/ui/session/SessionScreen.kt
index 995fa6d0..801f7c07 100644
--- a/apps/android/app/src/main/java/com/codedeck/plus/ui/session/SessionScreen.kt
+++ b/apps/android/app/src/main/java/com/codedeck/plus/ui/session/SessionScreen.kt
@@ -4,8 +4,8 @@ import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.SystemClock
-import android.util.Log
import android.speech.RecognizerIntent
+import android.util.Log
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
@@ -182,8 +182,8 @@ fun SessionScreen(
core: CoreHost,
machine: String,
sessionId: String,
- onBack: (() -> Unit)? = null,
modifier: Modifier = Modifier,
+ onBack: (() -> Unit)? = null,
) {
val machinesView by core.machines.collectAsState()
val uiView by core.ui.collectAsState()
diff --git a/apps/android/app/src/main/java/com/codedeck/plus/ui/transcript/MarkdownBlocks.kt b/apps/android/app/src/main/java/com/codedeck/plus/ui/transcript/MarkdownBlocks.kt
index e9aa58a7..ef5f4132 100644
--- a/apps/android/app/src/main/java/com/codedeck/plus/ui/transcript/MarkdownBlocks.kt
+++ b/apps/android/app/src/main/java/com/codedeck/plus/ui/transcript/MarkdownBlocks.kt
@@ -61,7 +61,7 @@ fun markdownBlocks(text: String, target: Int = BLOCK_TARGET): List {
}
append(line)
} else {
- val open = fence!!
+ val open = fence
if (trimmed.startsWith(open) && trimmed.trimEnd().all { it == open[0] }) {
fence = null
append(line)
diff --git a/apps/android/app/src/main/java/com/codedeck/plus/ui/transcript/rows/ToolGroupRow.kt b/apps/android/app/src/main/java/com/codedeck/plus/ui/transcript/rows/ToolGroupRow.kt
index e54ad551..e7ac8786 100644
--- a/apps/android/app/src/main/java/com/codedeck/plus/ui/transcript/rows/ToolGroupRow.kt
+++ b/apps/android/app/src/main/java/com/codedeck/plus/ui/transcript/rows/ToolGroupRow.kt
@@ -9,13 +9,13 @@ import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.DriveFileMove
import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight
import androidx.compose.material.icons.outlined.Build
import androidx.compose.material.icons.outlined.ChatBubbleOutline
import androidx.compose.material.icons.outlined.Checklist
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Description
-import androidx.compose.material.icons.outlined.DriveFileMove
import androidx.compose.material.icons.outlined.Language
import androidx.compose.material.icons.outlined.Psychology
import androidx.compose.material.icons.outlined.Search
@@ -49,7 +49,7 @@ internal fun toolKindIcon(kind: String): ImageVector = when (kind) {
"read" -> Icons.Outlined.Visibility
"edit" -> Icons.Outlined.Description
"delete" -> Icons.Outlined.Delete
- "move" -> Icons.Outlined.DriveFileMove
+ "move" -> Icons.AutoMirrored.Outlined.DriveFileMove
"search" -> Icons.Outlined.Search
"execute" -> Icons.Outlined.Terminal
"think" -> Icons.Outlined.Checklist
diff --git a/apps/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/apps/android/app/src/main/res/mipmap-anydpi/ic_launcher.xml
similarity index 100%
rename from apps/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
rename to apps/android/app/src/main/res/mipmap-anydpi/ic_launcher.xml
diff --git a/apps/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/apps/android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
similarity index 100%
rename from apps/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
rename to apps/android/app/src/main/res/mipmap-anydpi/ic_launcher_round.xml
diff --git a/apps/android/app/src/test/java/com/codedeck/plus/ui/transcript/MarkdownParityTest.kt b/apps/android/app/src/test/java/com/codedeck/plus/ui/transcript/MarkdownParityTest.kt
index cf0b099e..543ed602 100644
--- a/apps/android/app/src/test/java/com/codedeck/plus/ui/transcript/MarkdownParityTest.kt
+++ b/apps/android/app/src/test/java/com/codedeck/plus/ui/transcript/MarkdownParityTest.kt
@@ -32,7 +32,7 @@ class MarkdownParityTest {
)
@Composable
- private fun dark(content: @Composable () -> Unit) {
+ private fun Dark(content: @Composable () -> Unit) {
CodeDeckTheme {
Surface(color = Tokens.Bg, contentColor = Tokens.Text) {
Column(Modifier.background(Tokens.Bg).fillMaxWidth().padding(16.dp)) { content() }
@@ -42,6 +42,6 @@ class MarkdownParityTest {
@Test
fun assistant_markdown_gfm_and_code() {
- paparazzi.snapshot { dark { TranscriptMarkdown(MarkdownCorpus.ASSISTANT_MARKDOWN) } }
+ paparazzi.snapshot { Dark { TranscriptMarkdown(MarkdownCorpus.ASSISTANT_MARKDOWN) } }
}
}
diff --git a/apps/android/app/src/test/java/com/codedeck/plus/ui/transcript/TranscriptRowsParityTest.kt b/apps/android/app/src/test/java/com/codedeck/plus/ui/transcript/TranscriptRowsParityTest.kt
index 20ea7532..addec0b8 100644
--- a/apps/android/app/src/test/java/com/codedeck/plus/ui/transcript/TranscriptRowsParityTest.kt
+++ b/apps/android/app/src/test/java/com/codedeck/plus/ui/transcript/TranscriptRowsParityTest.kt
@@ -48,7 +48,7 @@ class TranscriptRowsParityTest {
)
@Composable
- private fun dark(content: @Composable () -> Unit) {
+ private fun Dark(content: @Composable () -> Unit) {
CodeDeckTheme {
Surface(color = Tokens.Bg, contentColor = Tokens.Text) {
Column(Modifier.background(Tokens.Bg).fillMaxWidth().padding(16.dp)) { content() }
@@ -75,7 +75,7 @@ class TranscriptRowsParityTest {
added = 0, removed = 0, failed = 0,
)
paparazzi.snapshot {
- dark {
+ Dark {
Column {
groups.forEach { ToolGroupRow(it, live = false, onOpen = {}) }
ToolGroupRow(lone, live = true, onOpen = {})
@@ -91,7 +91,7 @@ class TranscriptRowsParityTest {
}
@Composable
- private fun sheet(content: @Composable () -> Unit) {
+ private fun Sheet(content: @Composable () -> Unit) {
CodeDeckTheme {
Surface(color = Tokens.SurfaceRaised, contentColor = Tokens.Text) {
Column(Modifier.background(Tokens.SurfaceRaised).fillMaxWidth().padding(top = 16.dp)) { content() }
@@ -102,19 +102,19 @@ class TranscriptRowsParityTest {
@Test
fun tool_sheet_timeline() {
val work = corpus().filterIsInstance()[1]
- paparazzi.snapshot { sheet { ToolSheetContent(work, live = false, openPath = emptyList(), onOpenPath = {}, onClose = {}) } }
+ paparazzi.snapshot { Sheet { ToolSheetContent(work, live = false, openPath = emptyList(), onOpenPath = {}, onClose = {}) } }
}
@Test
fun tool_sheet_edit_page() {
val work = editGroup()
- paparazzi.snapshot { sheet { ToolSheetContent(work, live = false, openPath = listOf(work.steps[0].seq), onOpenPath = {}, onClose = {}) } }
+ paparazzi.snapshot { Sheet { ToolSheetContent(work, live = false, openPath = listOf(work.steps[0].seq), onOpenPath = {}, onClose = {}) } }
}
@Test
fun tool_sheet_failed_command_page() {
val work = editGroup()
- paparazzi.snapshot { sheet { ToolSheetContent(work, live = false, openPath = listOf(work.steps[1].seq), onOpenPath = {}, onClose = {}) } }
+ paparazzi.snapshot { Sheet { ToolSheetContent(work, live = false, openPath = listOf(work.steps[1].seq), onOpenPath = {}, onClose = {}) } }
}
/** The group with an edit and a failed command. */
@@ -129,19 +129,19 @@ class TranscriptRowsParityTest {
@Test
fun tool_sheet_agent_page() {
val group = agentGroup()
- paparazzi.snapshot { sheet { ToolSheetContent(group, live = true, openPath = listOf(group.steps[0].seq), onOpenPath = {}, onClose = {}) } }
+ paparazzi.snapshot { Sheet { ToolSheetContent(group, live = true, openPath = listOf(group.steps[0].seq), onOpenPath = {}, onClose = {}) } }
}
@Test
fun tool_sheet_agent_timeline() {
val group = agentGroup()
- paparazzi.snapshot { sheet { ToolSheetContent(group, live = true, openPath = emptyList(), onOpenPath = {}, onClose = {}) } }
+ paparazzi.snapshot { Sheet { ToolSheetContent(group, live = true, openPath = emptyList(), onOpenPath = {}, onClose = {}) } }
}
@Test
fun tool_sheet_plan_page() {
val group = agentGroup()
- paparazzi.snapshot { sheet { ToolSheetContent(group, live = false, openPath = listOf(group.steps[1].seq), onOpenPath = {}, onClose = {}) } }
+ paparazzi.snapshot { Sheet { ToolSheetContent(group, live = false, openPath = listOf(group.steps[1].seq), onOpenPath = {}, onClose = {}) } }
}
private fun activity(): ActivityView {
@@ -154,7 +154,7 @@ class TranscriptRowsParityTest {
@Test
fun activity_sheet() {
paparazzi.snapshot {
- sheet { ActivitySheetContent(activity(), live = false, canStop = true, onOpenAgent = {}, onStopTask = {}, onClose = {}) }
+ Sheet { ActivitySheetContent(activity(), live = false, canStop = true, onOpenAgent = {}, onStopTask = {}, onClose = {}) }
}
}
@@ -162,7 +162,7 @@ class TranscriptRowsParityTest {
fun activity_bar() {
val a = activity()
paparazzi.snapshot {
- dark {
+ Dark {
Column {
ActivityBar(a, live = false, onOpen = {})
// No plan: what the sub-agent does leads.
@@ -175,14 +175,14 @@ class TranscriptRowsParityTest {
@Test
fun diff_card() {
val diff = corpus().filterIsInstance().first()
- paparazzi.snapshot { dark { DiffRow(diff.path, diff.lines, diff.truncated, expanded = false, onToggle = {}) } }
+ paparazzi.snapshot { Dark { DiffRow(diff.path, diff.lines, diff.truncated, expanded = false, onToggle = {}) } }
}
@Test
fun permission_card_pending() {
val permission = corpus().filterIsInstance().last()
paparazzi.snapshot {
- dark { PermissionCard(permission, "machine", "session", responded = false, actions = {}) }
+ Dark { PermissionCard(permission, "machine", "session", responded = false, actions = {}) }
}
}
@@ -190,7 +190,7 @@ class TranscriptRowsParityTest {
fun plan_approval_card_pending() {
val plan = corpus().filterIsInstance().first()
paparazzi.snapshot {
- dark { PlanApprovalCard(plan, "machine", "session", responded = false, choice = null, actions = {}) }
+ Dark { PlanApprovalCard(plan, "machine", "session", responded = false, choice = null, actions = {}) }
}
}
@@ -198,7 +198,7 @@ class TranscriptRowsParityTest {
fun question_card_pending() {
val question = corpus().filterIsInstance().first()
paparazzi.snapshot {
- dark { QuestionCard(question, "machine", "session", respondedCards = emptySet(), actions = {}) }
+ Dark { QuestionCard(question, "machine", "session", respondedCards = emptySet(), actions = {}) }
}
}
@@ -206,7 +206,7 @@ class TranscriptRowsParityTest {
fun question_group_card_pending() {
val group = corpus().filterIsInstance().first { it.questions.size > 1 }
paparazzi.snapshot {
- dark { QuestionCard(group, "machine", "session", respondedCards = emptySet(), actions = {}) }
+ Dark { QuestionCard(group, "machine", "session", respondedCards = emptySet(), actions = {}) }
}
}
}
diff --git a/apps/android/app/src/test/snapshots/images/com.codedeck.plus.ui.transcript_MarkdownParityTest_assistant_markdown_gfm_and_code.png b/apps/android/app/src/test/snapshots/images/com.codedeck.plus.ui.transcript_MarkdownParityTest_assistant_markdown_gfm_and_code.png
index 2a37d782..f29cf4ba 100644
Binary files a/apps/android/app/src/test/snapshots/images/com.codedeck.plus.ui.transcript_MarkdownParityTest_assistant_markdown_gfm_and_code.png and b/apps/android/app/src/test/snapshots/images/com.codedeck.plus.ui.transcript_MarkdownParityTest_assistant_markdown_gfm_and_code.png differ
diff --git a/apps/android/gradle/libs.versions.toml b/apps/android/gradle/libs.versions.toml
index 3baa873d..0526bb96 100644
--- a/apps/android/gradle/libs.versions.toml
+++ b/apps/android/gradle/libs.versions.toml
@@ -3,9 +3,9 @@
# apps/mobile's Tauri-generated gen/android happens to sit on (AGP 8.11/
# Kotlin 1.9.25/compileSdk 36), which is Tauri's own scaffold and is
# regenerated on demand rather than kept current.
-agp = "9.4.0"
+agp = "9.4.1"
kotlin = "2.4.20"
-coreKtx = "1.19.0"
+coreKtx = "1.19.1"
lifecycle = "2.11.0"
activityCompose = "1.13.0"
composeBom = "2026.09.00"
@@ -13,25 +13,38 @@ coroutines = "1.11.0"
# Decodes the JSON `crates/client-ffi`'s `views.rs` crosses for
# the already-grouped transcript (`display_entries_json`) and the pending-
# permission summary — see `ui/transcript/DisplayEntries.kt`.
-kotlinxSerialization = "1.9.0"
+kotlinxSerialization = "1.11.0"
junit = "4.13.2"
androidxTestExtJunit = "1.3.0"
androidxTestRunner = "1.7.0"
androidxTestCore = "1.7.0"
-robolectric = "4.16"
+robolectric = "4.17"
# 1.3.5 was proven on an older Kotlin/AGP pair (2.0.21/8.7.3), but its
# Gradle plugin calls AGP's
# `BaseExtension`, removed in AGP 9 — a hard configuration-time failure, not
# a warning. 2.0.0-alpha05 is the first release with AGP 9 / the new
# `com.android.application`-without-`BaseExtension` API support; still
# pre-1.0, tracked here rather than pinned quietly.
-paparazzi = "2.0.0-alpha05"
-# 0.43.0: current as of this pass. Since 0.33.0 `Markdown(content)` parses
+paparazzi = "2.0.0-alpha05.1"
+# Since 0.33.0 `Markdown(content)` parses
# asynchronously by default (breaks a static Paparazzi snapshot) —
# `rememberMarkdownState(content, immediate = true)` is the documented
# synchronous escape hatch; used instead of pinning back to a version that
# predates table rendering.
-markdownRenderer = "0.43.0"
+markdownRenderer = "0.45.0"
+# The FFI bridge uniffi-bindgen's generated Kotlin calls crates/client-ffi's
+# cdylib through: keep it in step with the `uniffi` version client-ffi pins.
+jna = "5.19.1"
+# Tink directly, not its deprecated androidx.security:security-crypto wrapper
+# (frozen at 1.1.0-alpha07): platform/SecureIdentityStore.kt keeps the
+# identity secret encrypted at rest under a Keystore-held master key.
+tink = "1.23.0"
+# Pairing-QR scan (ui/screens/PairingScanView.kt): CameraX for the preview and
+# analysis, ZXing's core decoder for the code. Not ML Kit, which brings Play
+# Services, Firebase components and a usage-reporting upload into every
+# launch to read one QR code. Neither is covered by the Compose BOM.
+camerax = "1.6.2"
+zxing = "3.5.4"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -56,6 +69,14 @@ androidx-test-core = { group = "androidx.test", name = "core", version.ref = "an
robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" }
markdown-renderer = { group = "com.mikepenz", name = "multiplatform-markdown-renderer-android", version.ref = "markdownRenderer" }
markdown-renderer-m3 = { group = "com.mikepenz", name = "multiplatform-markdown-renderer-m3", version.ref = "markdownRenderer" }
+# Versioned by the Compose BOM.
+androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" }
+jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
+tink-android = { module = "com.google.crypto.tink:tink-android", version.ref = "tink" }
+androidx-camera-camera2 = { group = "androidx.camera", name = "camera-camera2", version.ref = "camerax" }
+androidx-camera-lifecycle = { group = "androidx.camera", name = "camera-lifecycle", version.ref = "camerax" }
+androidx-camera-view = { group = "androidx.camera", name = "camera-view", version.ref = "camerax" }
+zxing-core = { module = "com.google.zxing:core", version.ref = "zxing" }
[bundles]
compose = [
diff --git a/apps/android/gradle/wrapper/gradle-wrapper.jar b/apps/android/gradle/wrapper/gradle-wrapper.jar
index eddabd2e..5097068a 100644
Binary files a/apps/android/gradle/wrapper/gradle-wrapper.jar and b/apps/android/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/apps/android/gradle/wrapper/gradle-wrapper.properties b/apps/android/gradle/wrapper/gradle-wrapper.properties
index ad7845be..1eca32e7 100644
--- a/apps/android/gradle/wrapper/gradle-wrapper.properties
+++ b/apps/android/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.1-bin.zip
+distributionSha256Sum=bafd5ce9cfaea0fbccfdc8439a1ac42fbd4cd9c89dc9a988228d8a2639a58e6c
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
diff --git a/apps/android/gradlew.bat b/apps/android/gradlew.bat
index 8508ef68..03754c4c 100644
--- a/apps/android/gradlew.bat
+++ b/apps/android/gradlew.bat
@@ -26,6 +26,33 @@
@rem Set local scope for the variables, and ensure extensions are enabled
setlocal EnableExtensions
+@rem Catch executions from older scripts and ensure they exit cleanly.
+@rem This can be removed once we can be reasonably confident that few people
+@rem will be migrating directly to this new wrapper.
+goto afterSafetyNet
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+goto exitWithErrorLevel
+:afterSafetyNet
+
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
@@ -45,13 +72,14 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
-echo. 1>&2
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
+1>&2 echo.
+1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+1>&2 echo.
+1>&2 echo Please set the JAVA_HOME variable in your environment to match the
+1>&2 echo location of your Java installation.
"%COMSPEC%" /c exit 1
+goto exitWithErrorLevel
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
@@ -59,13 +87,14 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
-echo. 1>&2
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
+1>&2 echo.
+1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+1>&2 echo.
+1>&2 echo Please set the JAVA_HOME variable in your environment to match the
+1>&2 echo location of your Java installation.
"%COMSPEC%" /c exit 1
+goto exitWithErrorLevel
:execute
@rem Setup the command line
@@ -75,8 +104,9 @@ echo location of your Java installation. 1>&2
@rem Execute gradlew
@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
-endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
+endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel & goto exitWithErrorLevel
+@rem This label must not be changed. We rely on old scripts being able to jump to this point.
:exitWithErrorLevel
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
"%COMSPEC%" /c exit %ERRORLEVEL%
diff --git a/crates/bridge-core/src/session.rs b/crates/bridge-core/src/session.rs
index 74ca229d..e338fb9f 100644
--- a/crates/bridge-core/src/session.rs
+++ b/crates/bridge-core/src/session.rs
@@ -2,7 +2,7 @@
//! through it.
//!
//! A session is its persisted [`SessionRecord`] plus, while it runs, a
-//! [`Runner`]: where it is in its life, the cards waiting on the user, and
+//! `Runner`: where it is in its life, the cards waiting on the user, and
//! the bookkeeping for its input and output. The agent itself runs in the
//! agent host; a runner only mirrors it.
diff --git a/crates/bridge-runtime/src/direct.rs b/crates/bridge-runtime/src/direct.rs
index 35d6090a..575fb7d8 100644
--- a/crates/bridge-runtime/src/direct.rs
+++ b/crates/bridge-runtime/src/direct.rs
@@ -8,8 +8,8 @@
//! service to forward to.
//!
//! A connection must finish its TLS and WebSocket handshakes within
-//! [`HANDSHAKE_TIMEOUT`], then answer the challenge with a paired identity's
-//! HELLO within [`HELLO_TIMEOUT`]. Then it gets every event the bridge publishes
+//! `HANDSHAKE_TIMEOUT`, then answer the challenge with a paired identity's
+//! HELLO within `HELLO_TIMEOUT`. Then it gets every event the bridge publishes
//! for that identity (and those of the last hour since its resume point),
//! and its command events go to the engine exactly as a relay's would: the
//! engine drops one it already saw by its id. Unpairing a phone closes its
diff --git a/crates/bridge-runtime/src/main.rs b/crates/bridge-runtime/src/main.rs
index c7d7450a..e3cc12d4 100644
--- a/crates/bridge-runtime/src/main.rs
+++ b/crates/bridge-runtime/src/main.rs
@@ -34,7 +34,7 @@ struct Cli {
/// Relay URL, repeatable [env: CODEDECK_RELAYS, comma-separated]
#[arg(long = "relay", global = true)]
relays: Vec,
- /// Workspace root sessions may run in, repeatable [env: CODEDECK_WORKSPACE_ROOTS] [default: /workspaces]
+ /// Workspace root sessions may run in, repeatable [env: CODEDECK_WORKSPACE_ROOTS] (default: the workspaces folder in --home)
#[arg(long = "workspace", global = true)]
workspaces: Vec,
/// Path to the claude executable [env: CODEDECK_CLAUDE_PATH]
diff --git a/crates/client-core/src/bridge_api.rs b/crates/client-core/src/bridge_api.rs
index e4d73702..143c705e 100644
--- a/crates/client-core/src/bridge_api.rs
+++ b/crates/client-core/src/bridge_api.rs
@@ -148,7 +148,7 @@ pub enum InvalidStage {
Decode,
}
-/// A dropped payload, kept for diagnostics (capped at [`INVALID_RECORDS_CAP`]).
+/// A dropped payload, kept for diagnostics (capped at `INVALID_RECORDS_CAP`).
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidPayloadRecord {
pub event_id: String,
diff --git a/crates/client-ffi/src/signer.rs b/crates/client-ffi/src/signer.rs
index 187ff534..c7b8ff71 100644
--- a/crates/client-ffi/src/signer.rs
+++ b/crates/client-ffi/src/signer.rs
@@ -6,7 +6,7 @@
//! Rust-backed one for a key the app holds itself.
//!
//! Every call is blocking from Rust's point of view and runs on a worker
-//! thread ([`SignerAdapter`] hops to `spawn_blocking` first): an
+//! thread (`SignerAdapter` hops to `spawn_blocking` first): an
//! implementation may take as long as it needs, e.g. waiting for the user to
//! approve a request in the signer app, and must not touch main-thread
//! state.
diff --git a/crates/nostr-transport/src/ws.rs b/crates/nostr-transport/src/ws.rs
index 0fc7fb38..b66a8f82 100644
--- a/crates/nostr-transport/src/ws.rs
+++ b/crates/nostr-transport/src/ws.rs
@@ -1,5 +1,5 @@
//! `WsTransport` — the real [`Transport`] over WebSockets: one connect + read
-//! task per relay, wired through the pure [`frames`](super::frames) codec and
+//! task per relay, wired through the pure `frames` codec and
//! [`router`](super::router).
//!
//! Single-threaded by construction. The [`SubCallbacks`] closures a
@@ -25,14 +25,14 @@
//! * **ping liveness** — one pinger pings every relay together each
//! [`PING_EVERY`] (a host slows it while in the background, see
//! [`WsTransport::set_ping_interval`]), and a socket with no traffic for
-//! two missed pings ([`DEAD_AFTER`]) is dropped so a silently-rotted relay
+//! two missed pings (`DEAD_AFTER`) is dropped so a silently-rotted relay
//! is detected, not trusted. [`WsTransport::check_liveness`] runs the same
//! test on demand, for a host that wakes the device just to check. Reading and writing
//! run concurrently, so a write stuck on a full socket buffer can neither
//! stall inbound frames nor postpone that check; a write that stays stuck
-//! for [`WRITE_TIMEOUT`] drops the socket too.
+//! for `WRITE_TIMEOUT` drops the socket too.
//! * **bounded everything** — a dial (TCP, SOCKS5, TLS, WS handshake) has a
-//! deadline, and a relay's outbound queue holds at most [`OUTBOUND_QUEUE`]
+//! deadline, and a relay's outbound queue holds at most `OUTBOUND_QUEUE`
//! frames: a relay that cannot drain it is dropped and redialled rather than
//! buffered without limit.
//! * **NIP-42 AUTH** answered with the identity key; a REQ or EVENT refused
@@ -442,7 +442,7 @@ impl WsTransport {
}
/// Ping every relay each `interval` from now on, and give a silent socket
- /// [`dead_after`] that interval before dropping it. A phone slows this
+ /// `dead_after` that interval before dropping it. A phone slows this
/// down while its app is in the background: every ping is a radio
/// wake-up, and a background check is driven by
/// [`Self::check_liveness`] instead.
@@ -483,7 +483,7 @@ impl WsTransport {
/// — reported and redialled like any dead socket. Returns how many
/// answered. For a host that wakes the device briefly to check the
/// connection instead of holding it awake: a socket a NAT or the relay
- /// silently dropped is found at once, not after [`DEAD_AFTER`] of awake
+ /// silently dropped is found at once, not after `DEAD_AFTER` of awake
/// time.
pub async fn check_liveness(&self, within: Duration) -> usize {
let probes: Vec<(String, u64, Rc| >, u64)> = {
|