iris-android-app: transcript-screen feature -- I5's Android integration
Extends the existing tabs demo shell (I2/E5's Gradle project, JNI registration) with a second, mutually-exclusive AndroidAppState rather than building a third shell -- it already has the working IrisView/MainActivity Java and the register_view_class wiring, and the only thing a transcript screen needs on top is a different Client type (the same axis tabs_ui::build vs. transcript_ui::build already varies along on winit). `--no-default-features --features transcript-screen` builds transcript_client::TranscriptClient instead of the plain tabs Client: fetches the sandbox's session list, opens the first one, and follows it live, reusing desktop-app's app.rs shape (fold_event/group_tool_runs/ fold_page/raw_seq, a generation counter) almost verbatim. The one real difference is the redraw path -- android-view has no winit::EventLoopProxy, so Tasks gained redraw_handle() (iris/src/task.rs) to let a caller request a frame after each TaskCtx::update from inside a still-running task, not just once when the whole future completes. Deliberate simplification, not a template: there is no session list or enrollment UI here. build.rs bakes the sandbox's host/port/token plus the pinned CA in at build time from AI_APP_TRANSCRIPT_HOST/_PORT/_TOKEN and AI_APP_CA, the same trust-boundary reasoning as the Compose app's GeneratePinnedCert Gradle task, extended to also bake the enrollment since building a real one (Keystore-sealed storage, a QR/link scanner) is E3's scope, not this box's. Recorded in RUST.md's I5 box. tabs-ui and the transcript-screen deps are now both optional, gated by mutually exclusive tabs-screen (default) / transcript-screen features -- building one screen with the other's default deps still active tripped Cargo's unused_dependencies lint. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
78aff64844
commit
aa3d11471f
6 files changed
+820
-4
No files matched your search
Generated
+363
@@ -558,6 +558,12 @@ dependencies = [
|
|||||||
"arrayvec",
|
"arrayvec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.23.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bit-set"
|
name = "bit-set"
|
||||||
version = "0.8.0"
|
version = "0.8.0"
|
||||||
@@ -734,6 +740,16 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "client-core"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"event-model",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"ureq",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "clipboard-win"
|
name = "clipboard-win"
|
||||||
version = "5.4.1"
|
version = "5.4.1"
|
||||||
@@ -779,6 +795,35 @@ dependencies = [
|
|||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie"
|
||||||
|
version = "0.18.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
"time",
|
||||||
|
"version_check",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cookie_store"
|
||||||
|
version = "0.22.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
|
||||||
|
dependencies = [
|
||||||
|
"cookie",
|
||||||
|
"document-features",
|
||||||
|
"idna",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"time",
|
||||||
|
"url",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.9.4"
|
version = "0.9.4"
|
||||||
@@ -886,6 +931,12 @@ version = "1.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dispatch"
|
name = "dispatch"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -1048,6 +1099,14 @@ dependencies = [
|
|||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "event-model"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "exr"
|
name = "exr"
|
||||||
version = "1.74.2"
|
version = "1.74.2"
|
||||||
@@ -1179,6 +1238,15 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "form_urlencoded"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
|
||||||
|
dependencies = [
|
||||||
|
"percent-encoding",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.34"
|
version = "0.3.34"
|
||||||
@@ -1253,6 +1321,26 @@ dependencies = [
|
|||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getopts"
|
||||||
|
version = "0.2.24"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"wasi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.3.4"
|
version = "0.3.4"
|
||||||
@@ -1423,6 +1511,22 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
|
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http"
|
||||||
|
version = "1.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"itoa",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httparse"
|
||||||
|
version = "1.10.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
@@ -1551,6 +1655,27 @@ version = "2.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad"
|
checksum = "ae293c039020f9ec10710af98d29ce6aa2051486638b49c9a6409f3b4a9e98ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
|
||||||
|
dependencies = [
|
||||||
|
"idna_adapter",
|
||||||
|
"smallvec",
|
||||||
|
"utf8_iter",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna_adapter"
|
||||||
|
version = "1.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
|
||||||
|
dependencies = [
|
||||||
|
"icu_normalizer",
|
||||||
|
"icu_properties",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
version = "0.25.10"
|
version = "0.25.10"
|
||||||
@@ -1640,9 +1765,13 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"android-view",
|
"android-view",
|
||||||
"android_logger",
|
"android_logger",
|
||||||
|
"client-core",
|
||||||
|
"event-model",
|
||||||
"iris",
|
"iris",
|
||||||
"log",
|
"log",
|
||||||
|
"serde_json",
|
||||||
"tabs-ui",
|
"tabs-ui",
|
||||||
|
"transcript-ui",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1676,6 +1805,12 @@ dependencies = [
|
|||||||
"either",
|
"either",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jni"
|
name = "jni"
|
||||||
version = "0.21.1"
|
version = "0.21.1"
|
||||||
@@ -2096,6 +2231,12 @@ dependencies = [
|
|||||||
"num-traits",
|
"num-traits",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-derive"
|
name = "num-derive"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
@@ -2744,6 +2885,12 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ppv-lite86"
|
name = "ppv-lite86"
|
||||||
version = "0.2.21"
|
version = "0.2.21"
|
||||||
@@ -2796,6 +2943,25 @@ dependencies = [
|
|||||||
"syn 2.0.119",
|
"syn 2.0.119",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pulldown-cmark"
|
||||||
|
version = "0.13.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e9f068eba8e7071c5f9511831b44f32c740d5adf574e990f946ddb53db2f314e"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.13.1",
|
||||||
|
"getopts",
|
||||||
|
"memchr",
|
||||||
|
"pulldown-cmark-escape",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pulldown-cmark-escape"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pulp"
|
name = "pulp"
|
||||||
version = "0.22.3"
|
version = "0.22.3"
|
||||||
@@ -3075,6 +3241,20 @@ version = "0.8.53"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
|
checksum = "47b34b781b31e5d73e9fbc8689c70551fd1ade9a19e3e28cfec8580a79290cc4"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "roxmltree"
|
name = "roxmltree"
|
||||||
version = "0.21.1"
|
version = "0.21.1"
|
||||||
@@ -3125,6 +3305,41 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.23.43"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-webpki",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pki-types"
|
||||||
|
version = "1.15.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
|
||||||
|
dependencies = [
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.103.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
|
||||||
|
dependencies = [
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
@@ -3207,6 +3422,19 @@ dependencies = [
|
|||||||
"syn 3.0.5",
|
"syn 3.0.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.151"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_repr"
|
name = "serde_repr"
|
||||||
version = "0.1.21"
|
version = "0.1.21"
|
||||||
@@ -3363,6 +3591,12 @@ version = "0.1.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "swash"
|
name = "swash"
|
||||||
version = "0.2.10"
|
version = "0.2.10"
|
||||||
@@ -3490,6 +3724,36 @@ dependencies = [
|
|||||||
"zune-jpeg",
|
"zune-jpeg",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.55"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"num-conv",
|
||||||
|
"powerfmt",
|
||||||
|
"serde_core",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.32"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tiny-skia"
|
name = "tiny-skia"
|
||||||
version = "0.11.4"
|
version = "0.11.4"
|
||||||
@@ -3596,6 +3860,16 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "transcript-ui"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"client-core",
|
||||||
|
"event-model",
|
||||||
|
"iris",
|
||||||
|
"pulldown-cmark",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tree_magic_mini"
|
name = "tree_magic_mini"
|
||||||
version = "3.2.2"
|
version = "3.2.2"
|
||||||
@@ -3634,6 +3908,12 @@ dependencies = [
|
|||||||
"keyboard-types",
|
"keyboard-types",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
@@ -3652,6 +3932,62 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq"
|
||||||
|
version = "3.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"cookie_store",
|
||||||
|
"flate2",
|
||||||
|
"log",
|
||||||
|
"percent-encoding",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"ureq-proto",
|
||||||
|
"utf8-zero",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ureq-proto"
|
||||||
|
version = "0.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"http",
|
||||||
|
"httparse",
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "url"
|
||||||
|
version = "2.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"idna",
|
||||||
|
"percent-encoding",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "utf8-zero"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "utf8_iter"
|
name = "utf8_iter"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
@@ -3696,6 +4032,12 @@ dependencies = [
|
|||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasi"
|
||||||
|
version = "0.11.1+wasi-snapshot-preview1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasip2"
|
name = "wasip2"
|
||||||
version = "1.0.4+wasi-0.2.12"
|
version = "1.0.4+wasi-0.2.12"
|
||||||
@@ -3889,6 +4231,15 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "weezl"
|
name = "weezl"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
@@ -4748,6 +5099,12 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.5"
|
version = "0.2.5"
|
||||||
@@ -4789,6 +5146,12 @@ version = "0.6.7"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
|
checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zune-core"
|
name = "zune-core"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
|
|||||||
@@ -17,10 +17,26 @@ crate-type = ["cdylib"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
iris = { path = "../" }
|
iris = { path = "../" }
|
||||||
tabs-ui = { path = "../tabs-ui" }
|
|
||||||
android-view = { git = "https://github.com/rust-mobile/android-view.git", rev = "bec6c62a96cef8239b0fd7fedeef9b184d02e3a1" }
|
android-view = { git = "https://github.com/rust-mobile/android-view.git", rev = "bec6c62a96cef8239b0fd7fedeef9b184d02e3a1" }
|
||||||
android_logger = "0.15.0"
|
android_logger = "0.15.0"
|
||||||
log = "0.4.28"
|
log = "0.4.28"
|
||||||
|
# `tabs-screen` (default, I2/I4's demo) and `transcript-screen` (I5's
|
||||||
|
# Android integration) are mutually exclusive -- one `ActiveClient` type is
|
||||||
|
# compiled in, never both (`lib.rs`'s doc comment) -- so both sets of deps
|
||||||
|
# are optional and each screen's feature pulls in only its own. Without
|
||||||
|
# this, building `--features transcript-screen` alone (default features
|
||||||
|
# still on) left `tabs-ui` linked but never referenced under that cfg,
|
||||||
|
# which Cargo's `unused_dependencies` lint (on by default) correctly flags.
|
||||||
|
tabs-ui = { path = "../tabs-ui", optional = true }
|
||||||
|
transcript-ui = { path = "../transcript-ui", optional = true }
|
||||||
|
client-core = { path = "../../client-core", optional = true }
|
||||||
|
event-model = { path = "../../event-model", optional = true }
|
||||||
|
serde_json = { version = "1", features = ["float_roundtrip"], optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["tabs-screen"]
|
||||||
|
tabs-screen = ["dep:tabs-ui"]
|
||||||
|
transcript-screen = ["dep:transcript-ui", "dep:client-core", "dep:event-model", "dep:serde_json"]
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
// Only does anything under the `transcript-screen` feature (RUST.md's I5
|
||||||
|
// Android integration) -- the plain tabs build (I2/I4) needs none of this
|
||||||
|
// and stays untouched, same reasoning as the feature gate in Cargo.toml.
|
||||||
|
//
|
||||||
|
// Bakes the sandbox server's host, port, token and pinned CA in at build
|
||||||
|
// time, the same way `app/androidApp/build.gradle.kts`'s
|
||||||
|
// `GeneratePinnedCert` task bakes the CA for the Compose app -- see that
|
||||||
|
// file's comment for why reading the machine's own certificate at build
|
||||||
|
// time is the right trust boundary. This build additionally bakes the
|
||||||
|
// host/port/token, which the Compose app does not: that app enrolls at
|
||||||
|
// runtime from a scanned QR/deep link, and a from-scratch enrollment UI
|
||||||
|
// (Keystore-sealed token storage, a QR/link scanner) is real, separate
|
||||||
|
// scope this integration does not need to build to answer RUST.md's
|
||||||
|
// question -- there is nothing here yet resembling `ServerConfig.kt`. So
|
||||||
|
// this is a **deliberate simplification for this rig only**: an APK built
|
||||||
|
// this way is good for exactly the emulator/server pair that built it, and
|
||||||
|
// must never be treated as a template for a real enrollment flow. Recorded
|
||||||
|
// in RUST.md's I5 box rather than left to be rediscovered.
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if std::env::var_os("CARGO_FEATURE_TRANSCRIPT_SCREEN").is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
println!("cargo:rerun-if-env-changed=AI_APP_TRANSCRIPT_HOST");
|
||||||
|
println!("cargo:rerun-if-env-changed=AI_APP_TRANSCRIPT_PORT");
|
||||||
|
println!("cargo:rerun-if-env-changed=AI_APP_TRANSCRIPT_TOKEN");
|
||||||
|
println!("cargo:rerun-if-env-changed=AI_APP_CA");
|
||||||
|
println!("cargo:rerun-if-env-changed=XDG_CONFIG_HOME");
|
||||||
|
|
||||||
|
let host = require_env(
|
||||||
|
"AI_APP_TRANSCRIPT_HOST",
|
||||||
|
"the sandbox server's host as the emulator reaches it, e.g. 10.0.2.2",
|
||||||
|
);
|
||||||
|
let port = require_env(
|
||||||
|
"AI_APP_TRANSCRIPT_PORT",
|
||||||
|
"the sandbox server's port -- app/ui-sandbox.sh's start banner prints it",
|
||||||
|
);
|
||||||
|
let token = require_env(
|
||||||
|
"AI_APP_TRANSCRIPT_TOKEN",
|
||||||
|
"the bearer token -- ~/.config/ai-app/sandbox-token, or the start banner's enrollment link",
|
||||||
|
);
|
||||||
|
|
||||||
|
let ca_path = std::env::var_os("AI_APP_CA")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let base = std::env::var_os("XDG_CONFIG_HOME")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
let home = std::env::var_os("HOME").expect("HOME must be set");
|
||||||
|
PathBuf::from(home).join(".config")
|
||||||
|
});
|
||||||
|
base.join("ai-app").join("certs").join("ca.pem")
|
||||||
|
});
|
||||||
|
let ca_pem = std::fs::read_to_string(&ca_path).unwrap_or_else(|e| {
|
||||||
|
panic!(
|
||||||
|
"no CA certificate at {} ({e}).\n\
|
||||||
|
Start ai-server (or app/ui-sandbox.sh) once on this machine first -- it \
|
||||||
|
generates the CA this build pins. Set AI_APP_CA=/path/to/ca.pem to build \
|
||||||
|
against a different one.",
|
||||||
|
ca_path.display()
|
||||||
|
)
|
||||||
|
});
|
||||||
|
let ca_pem = ca_pem.trim();
|
||||||
|
if !ca_pem.starts_with("-----BEGIN CERTIFICATE-----") {
|
||||||
|
panic!("{} is not a PEM certificate.", ca_path.display());
|
||||||
|
}
|
||||||
|
|
||||||
|
let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
|
||||||
|
let generated = format!(
|
||||||
|
"// Generated by build.rs from {host}:{port} and {ca}. Do not edit.\n\
|
||||||
|
pub const HOST: &str = {host_lit:?};\n\
|
||||||
|
pub const PORT: u16 = {port};\n\
|
||||||
|
pub const TOKEN: &str = {token_lit:?};\n\
|
||||||
|
pub const CA_PEM: &str = {ca_lit:?};\n",
|
||||||
|
host = host,
|
||||||
|
port = port
|
||||||
|
.parse::<u16>()
|
||||||
|
.unwrap_or_else(|e| panic!("AI_APP_TRANSCRIPT_PORT={port:?} is not a u16: {e}")),
|
||||||
|
ca = ca_path.display(),
|
||||||
|
host_lit = host,
|
||||||
|
token_lit = token,
|
||||||
|
ca_lit = ca_pem,
|
||||||
|
);
|
||||||
|
std::fs::write(out_dir.join("pinned_config.rs"), generated).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_env(name: &str, what: &str) -> String {
|
||||||
|
std::env::var(name).unwrap_or_else(|_| {
|
||||||
|
panic!("{name} must be set to build the transcript-screen feature -- {what}")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
//! The android-view demo app: iris's `tabs` widget tree (`tabs_ui::build`,
|
//! The android-view demo app: by default, iris's `tabs` widget tree
|
||||||
//! shared with the winit example) running through
|
//! (`tabs_ui::build`, shared with the winit example) running through
|
||||||
//! `iris::android`'s `ViewPeer`. This is RUST.md's I2 pass condition made
|
//! `iris::android`'s `ViewPeer`. This is RUST.md's I2 pass condition made
|
||||||
//! concrete -- there is no UI here beyond what `tabs-ui` already draws.
|
//! concrete -- there is no UI here beyond what `tabs-ui` already draws.
|
||||||
//!
|
//!
|
||||||
@@ -9,6 +9,20 @@
|
|||||||
//! wrapping `iris::android::new_peer`'s generic function in a concrete
|
//! wrapping `iris::android::new_peer`'s generic function in a concrete
|
||||||
//! `extern "system" fn`, since `register_view_class` wants a plain
|
//! `extern "system" fn`, since `register_view_class` wants a plain
|
||||||
//! function pointer.
|
//! function pointer.
|
||||||
|
//!
|
||||||
|
//! **`transcript-screen` feature (RUST.md's I5 Android integration):** with
|
||||||
|
//! `--features transcript-screen`, `new_view_peer` instantiates
|
||||||
|
//! `transcript_client::TranscriptClient` instead of the tabs `Client`
|
||||||
|
//! below, against a real `ai-server` (see that module's doc). Chosen over a
|
||||||
|
//! third shell crate: this one already has the Gradle project, the
|
||||||
|
//! `IrisView`/`MainActivity` Java, and the JNI registration I2 built and
|
||||||
|
//! measured against, and the only thing a transcript screen needs on top
|
||||||
|
//! is a different `AndroidAppState` -- the same axis `tabs_ui::build` vs.
|
||||||
|
//! `transcript_ui::build` already varies along on the winit side (compare
|
||||||
|
//! `iris/examples/tabs.rs` and `iris/transcript-ui/examples/transcript.rs`).
|
||||||
|
//! A build picks one screen or the other, never both, so `Client` and
|
||||||
|
//! `TranscriptClient` are cfg-gated apart rather than switched at runtime --
|
||||||
|
//! there is no in-app navigation to switch *to* on either side yet.
|
||||||
|
|
||||||
use android_view::{
|
use android_view::{
|
||||||
Context, View,
|
Context, View,
|
||||||
@@ -18,19 +32,26 @@ use android_view::{
|
|||||||
},
|
},
|
||||||
register_view_class,
|
register_view_class,
|
||||||
};
|
};
|
||||||
|
#[cfg(not(feature = "transcript-screen"))]
|
||||||
use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState};
|
use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState};
|
||||||
|
#[cfg(not(feature = "transcript-screen"))]
|
||||||
use iris::prelude::*;
|
use iris::prelude::*;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use std::ffi::c_void;
|
use std::ffi::c_void;
|
||||||
|
|
||||||
|
#[cfg(feature = "transcript-screen")]
|
||||||
|
mod transcript_client;
|
||||||
|
|
||||||
/// The app's `View` subclass, matching the Java side's package --
|
/// The app's `View` subclass, matching the Java side's package --
|
||||||
/// `app/src/main/java/dev/iris/android/demo/IrisView.java`.
|
/// `app/src/main/java/dev/iris/android/demo/IrisView.java`.
|
||||||
const VIEW_CLASS: &str = "dev/iris/android/demo/IrisView";
|
const VIEW_CLASS: &str = "dev/iris/android/demo/IrisView";
|
||||||
|
|
||||||
|
#[cfg(not(feature = "transcript-screen"))]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
ui_state: AndroidUiState,
|
ui_state: AndroidUiState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "transcript-screen"))]
|
||||||
impl HasAndroidUiState for Client {
|
impl HasAndroidUiState for Client {
|
||||||
fn android_state(&self) -> &AndroidUiState {
|
fn android_state(&self) -> &AndroidUiState {
|
||||||
&self.ui_state
|
&self.ui_state
|
||||||
@@ -40,6 +61,7 @@ impl HasAndroidUiState for Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "transcript-screen"))]
|
||||||
impl AndroidAppState for Client {
|
impl AndroidAppState for Client {
|
||||||
fn new(mut ui_state: AndroidUiState, rsc: &mut AndroidRsc<Self>) -> Self {
|
fn new(mut ui_state: AndroidUiState, rsc: &mut AndroidRsc<Self>) -> Self {
|
||||||
// `widgets.info` is the winit example's frame-debug readout, kept
|
// `widgets.info` is the winit example's frame-debug readout, kept
|
||||||
@@ -61,12 +83,17 @@ impl AndroidAppState for Client {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "transcript-screen"))]
|
||||||
|
type ActiveClient = Client;
|
||||||
|
#[cfg(feature = "transcript-screen")]
|
||||||
|
type ActiveClient = transcript_client::TranscriptClient;
|
||||||
|
|
||||||
extern "system" fn new_view_peer<'local>(
|
extern "system" fn new_view_peer<'local>(
|
||||||
env: JNIEnv<'local>,
|
env: JNIEnv<'local>,
|
||||||
view: View<'local>,
|
view: View<'local>,
|
||||||
context: Context<'local>,
|
context: Context<'local>,
|
||||||
) -> jlong {
|
) -> jlong {
|
||||||
iris::android::new_peer::<Client>(env, view, context)
|
iris::android::new_peer::<ActiveClient>(env, view, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Safety
|
/// # Safety
|
||||||
|
|||||||
@@ -0,0 +1,306 @@
|
|||||||
|
//! RUST.md's I5 Android integration: `transcript-ui`'s screen filling the
|
||||||
|
//! whole window on android-view, against a real `ai-server` through
|
||||||
|
//! `client-core` -- the missing half `iris-android-app` (I2) only had for
|
||||||
|
//! `tabs-ui` until now. Behind the `transcript-screen` Cargo feature so the
|
||||||
|
//! plain build (`cargo ndk build`, no `--features`) stays exactly the tabs
|
||||||
|
//! demo I2/I4 already measured against.
|
||||||
|
//!
|
||||||
|
//! **Deliberate simplification, recorded rather than left to be
|
||||||
|
//! rediscovered (RUST.md's I5 box has the full account)**: there is no
|
||||||
|
//! session list and no enrollment UI here. The server, port, token and
|
||||||
|
//! pinned CA are baked in at build time (`build.rs`'s
|
||||||
|
//! `AI_APP_TRANSCRIPT_HOST`/`_PORT`/`_TOKEN`/`AI_APP_CA`), and the first
|
||||||
|
//! session `ApiClient::fetch_sessions` returns is opened automatically --
|
||||||
|
//! there is nothing to tap to get there, which is what `transcript-bench.sh`
|
||||||
|
//! and `ui-trace` need to land straight on the screen under test. A real
|
||||||
|
//! app needs `desktop-app`'s `EnrolledServer`/QR-link flow or E3's
|
||||||
|
//! Keystore-sealed `ServerConfig.kt`; building a second one of those was
|
||||||
|
//! not this pass's job.
|
||||||
|
//!
|
||||||
|
//! **Reuses `iris/desktop-app`'s `app.rs` shape almost exactly** --
|
||||||
|
//! `fold_event`/`group_tool_runs`/`fold_page`/`raw_seq` from
|
||||||
|
//! `client_core::transcript_fold`, a `generation` counter guarding against
|
||||||
|
//! a stale background response, and a full rebuild of the widget tree on
|
||||||
|
//! every event (same tradeoff, same reason: `push_row` cannot update a row
|
||||||
|
//! already on screen, and this rig's conversations are small). What
|
||||||
|
//! differs is only the redraw mechanism: android-view has no
|
||||||
|
//! `winit::EventLoopProxy`, so this uses `iris::task::Tasks::redraw_handle`
|
||||||
|
//! (new, added alongside this box) to request a frame after each
|
||||||
|
//! `TaskCtx::update` instead of relying on `Tasks::spawn`'s single
|
||||||
|
//! end-of-future redraw -- see that method's own doc for why.
|
||||||
|
|
||||||
|
use client_core::api::{ApiClient, UreqTransport};
|
||||||
|
use client_core::event_stream::{StreamItem, follow_session_events};
|
||||||
|
use client_core::transcript_fold::{TranscriptItem, fold_event, fold_page, group_tool_runs};
|
||||||
|
use event_model::SeqEvent;
|
||||||
|
use iris::android::{AndroidAppState, AndroidRsc, AndroidUiState, HasAndroidUiState};
|
||||||
|
use iris::prelude::*;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
|
mod pinned {
|
||||||
|
include!(concat!(env!("OUT_DIR"), "/pinned_config.rs"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TranscriptClient {
|
||||||
|
ui_state: AndroidUiState,
|
||||||
|
screen: Option<transcript_ui::TranscriptScreen>,
|
||||||
|
/// The folded transcript as of the last rebuild -- kept here (not
|
||||||
|
/// re-derived) for the same reason `desktop-app`'s `Client::items`
|
||||||
|
/// exists: a live `StreamEvent` only carries one new wire event, and
|
||||||
|
/// `fold_event` needs everything folded so far to fold it in.
|
||||||
|
items: Vec<TranscriptItem>,
|
||||||
|
/// The session currently open -- `None` only before the first fetch
|
||||||
|
/// resolves. Read back by `apply_event`'s rebuild, which has no session
|
||||||
|
/// id of its own (a live `SeqEvent` doesn't carry one).
|
||||||
|
session_id: Option<String>,
|
||||||
|
/// Bumped every time a new session load starts; a background response
|
||||||
|
/// checks it before touching state, so a slow reply for a session this
|
||||||
|
/// screen has moved on from can't overwrite what replaced it. There is
|
||||||
|
/// only ever one session here (no list to switch away to), but the
|
||||||
|
/// guard still matters for the *first* fetch racing a `stop`/`start`.
|
||||||
|
generation: Arc<AtomicU64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HasAndroidUiState for TranscriptClient {
|
||||||
|
fn android_state(&self) -> &AndroidUiState {
|
||||||
|
&self.ui_state
|
||||||
|
}
|
||||||
|
fn android_state_mut(&mut self) -> &mut AndroidUiState {
|
||||||
|
&mut self.ui_state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds one `UreqTransport` from the config `build.rs` baked in. Called
|
||||||
|
/// twice per session load, same as `desktop-app`'s `build_transport`
|
||||||
|
/// closure -- `ApiClient` and the live-stream follow each need their own,
|
||||||
|
/// since `UreqTransport` holds its own `ureq::Agent`.
|
||||||
|
fn build_transport() -> Result<UreqTransport, String> {
|
||||||
|
let base_url = format!("https://{}:{}", pinned::HOST, pinned::PORT);
|
||||||
|
UreqTransport::new(
|
||||||
|
base_url,
|
||||||
|
pinned::TOKEN.to_string(),
|
||||||
|
pinned::CA_PEM.as_bytes(),
|
||||||
|
)
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn placeholder<Rsc: HasEvents>(rsc: &mut Rsc, message: &str) -> StrongWidget {
|
||||||
|
wtext(message.to_string())
|
||||||
|
.color(Color::WHITE)
|
||||||
|
.wrap(true)
|
||||||
|
.pad(16)
|
||||||
|
.add_strong(rsc)
|
||||||
|
.any()
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AndroidAppState for TranscriptClient {
|
||||||
|
fn new(mut ui_state: AndroidUiState, rsc: &mut AndroidRsc<Self>) -> Self {
|
||||||
|
let loading = placeholder(rsc, "Loading sessions...");
|
||||||
|
ui_state.set_root(loading);
|
||||||
|
|
||||||
|
let mut client = Self {
|
||||||
|
ui_state,
|
||||||
|
screen: None,
|
||||||
|
items: Vec::new(),
|
||||||
|
session_id: None,
|
||||||
|
generation: Arc::new(AtomicU64::new(0)),
|
||||||
|
};
|
||||||
|
client.spawn_fetch_sessions(rsc);
|
||||||
|
client
|
||||||
|
}
|
||||||
|
|
||||||
|
fn back_pressed(&mut self, _rsc: &mut AndroidRsc<Self>, _render: &mut UiRenderState) -> bool {
|
||||||
|
// No screen stack of its own -- same "let the activity finish"
|
||||||
|
// answer `iris-android-app`'s tabs `Client` already gives.
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TranscriptClient {
|
||||||
|
fn show_message(&mut self, rsc: &mut AndroidRsc<Self>, message: &str) {
|
||||||
|
let widget = placeholder(rsc, message);
|
||||||
|
self.android_state_mut().set_root(widget);
|
||||||
|
self.screen = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_fetch_sessions(&mut self, rsc: &mut AndroidRsc<Self>) {
|
||||||
|
let redraw = rsc.tasks.redraw_handle();
|
||||||
|
let my_generation = self.generation.load(Ordering::SeqCst);
|
||||||
|
let generation = self.generation.clone();
|
||||||
|
rsc.spawn_task(async move |mut ctx| {
|
||||||
|
let outcome = match build_transport() {
|
||||||
|
Ok(transport) => ApiClient::new(transport)
|
||||||
|
.fetch_sessions()
|
||||||
|
.map_err(|e| e.to_string()),
|
||||||
|
Err(e) => Err(format!("couldn't set up TLS: {e}")),
|
||||||
|
};
|
||||||
|
ctx.update(move |state: &mut TranscriptClient, rsc| {
|
||||||
|
if generation.load(Ordering::SeqCst) != my_generation {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match outcome {
|
||||||
|
Ok(sessions) => match sessions.into_iter().next() {
|
||||||
|
Some(session) => state.select_session(rsc, session.id),
|
||||||
|
None => state.show_message(rsc, "No sessions on the sandbox server."),
|
||||||
|
},
|
||||||
|
Err(message) => {
|
||||||
|
state.show_message(rsc, &format!("Couldn't list sessions: {message}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
redraw.request_redraw();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the opening page, then follows the live SSE stream for the
|
||||||
|
/// rest of this session's life -- `desktop-app`'s `select_session`
|
||||||
|
/// almost verbatim, with `Proxy::send_event` replaced by `ctx.update` +
|
||||||
|
/// `redraw.request_redraw()` (see this module's doc).
|
||||||
|
fn select_session(&mut self, rsc: &mut AndroidRsc<Self>, session_id: String) {
|
||||||
|
let my_generation = self.generation.fetch_add(1, Ordering::SeqCst) + 1;
|
||||||
|
self.items.clear();
|
||||||
|
self.session_id = Some(session_id.clone());
|
||||||
|
self.show_message(rsc, "Loading transcript...");
|
||||||
|
|
||||||
|
let redraw = rsc.tasks.redraw_handle();
|
||||||
|
let live_generation = self.generation.clone();
|
||||||
|
rsc.spawn_task(async move |mut ctx| {
|
||||||
|
let transports =
|
||||||
|
build_transport().and_then(|rest| build_transport().map(|stream| (rest, stream)));
|
||||||
|
let (rest, stream_transport) = match transports {
|
||||||
|
Ok(pair) => pair,
|
||||||
|
Err(e) => {
|
||||||
|
let message = format!("couldn't set up TLS: {e}");
|
||||||
|
ctx.update(move |state: &mut TranscriptClient, rsc| {
|
||||||
|
if live_generation.load(Ordering::SeqCst) == my_generation {
|
||||||
|
state.show_message(rsc, &message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
redraw.request_redraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let api = ApiClient::new(rest);
|
||||||
|
|
||||||
|
// The most recent 200 events, coalesced -- the same page size
|
||||||
|
// `desktop-app` uses; RUST.md's I3/history-paging work is what
|
||||||
|
// a real scrollback would reuse (out of scope here, same as
|
||||||
|
// E4).
|
||||||
|
let page: Result<Vec<serde_json::Value>, String> = api
|
||||||
|
.fetch_transcript_page(&session_id, None, 200, true)
|
||||||
|
.map_err(|e| e.to_string());
|
||||||
|
// The wire `seq` of the last line, not a folded item's `seq()`
|
||||||
|
// -- see `client_core::transcript_fold::raw_seq`'s doc for why
|
||||||
|
// resuming from the latter re-delivers deltas already folded
|
||||||
|
// into an in-progress reply.
|
||||||
|
let after = page
|
||||||
|
.as_ref()
|
||||||
|
.ok()
|
||||||
|
.and_then(|values| values.last())
|
||||||
|
.and_then(client_core::transcript_fold::raw_seq)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let result = page.and_then(|values| fold_page(&values));
|
||||||
|
|
||||||
|
{
|
||||||
|
let live_generation = live_generation.clone();
|
||||||
|
ctx.update(move |state: &mut TranscriptClient, rsc| {
|
||||||
|
if live_generation.load(Ordering::SeqCst) != my_generation {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
match result {
|
||||||
|
Ok(items) => {
|
||||||
|
state.items = items;
|
||||||
|
state.rebuild_transcript(rsc);
|
||||||
|
}
|
||||||
|
Err(message) => {
|
||||||
|
state.show_message(rsc, &format!("Couldn't load transcript: {message}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
redraw.request_redraw();
|
||||||
|
|
||||||
|
if live_generation.load(Ordering::SeqCst) != my_generation {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The outer closure here is an `FnMut` -- `follow_session_events`
|
||||||
|
// calls it once per line -- so it captures `live_generation` by
|
||||||
|
// move and re-clones it for each inner `ctx.update` closure
|
||||||
|
// rather than moving a shared `stop`-style helper into itself:
|
||||||
|
// a value moved out of an `FnMut`'s captures on one call leaves
|
||||||
|
// nothing there for the next.
|
||||||
|
let _ =
|
||||||
|
follow_session_events(
|
||||||
|
&stream_transport,
|
||||||
|
&session_id,
|
||||||
|
after,
|
||||||
|
move |item| match item {
|
||||||
|
StreamItem::Open | StreamItem::Reset => {
|
||||||
|
live_generation.load(Ordering::SeqCst) == my_generation
|
||||||
|
}
|
||||||
|
StreamItem::Event { event, .. } => {
|
||||||
|
if live_generation.load(Ordering::SeqCst) != my_generation {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let live_generation = live_generation.clone();
|
||||||
|
ctx.update(move |state: &mut TranscriptClient, rsc| {
|
||||||
|
if live_generation.load(Ordering::SeqCst) != my_generation {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.apply_event(rsc, &event);
|
||||||
|
});
|
||||||
|
redraw.request_redraw();
|
||||||
|
true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rebuilds the whole widget tree from `self.items` -- same tradeoff as
|
||||||
|
/// `desktop-app`'s `rebuild_transcript` (this module's doc comment).
|
||||||
|
/// Reads `self.session_id` rather than taking one, since every caller
|
||||||
|
/// (the opening page, and every live event) already has it set there.
|
||||||
|
fn rebuild_transcript(&mut self, rsc: &mut AndroidRsc<Self>) {
|
||||||
|
let in_progress = self
|
||||||
|
.screen
|
||||||
|
.as_ref()
|
||||||
|
.map(|screen| screen.composer.field.edit(rsc).text.text().to_string())
|
||||||
|
.filter(|t| !t.is_empty());
|
||||||
|
|
||||||
|
let rows = group_tool_runs(&self.items);
|
||||||
|
let (screen, tree) = transcript_ui::build_tree(rsc, rows);
|
||||||
|
|
||||||
|
if let Some(text) = in_progress {
|
||||||
|
screen.composer.field.edit(rsc).set(&text);
|
||||||
|
}
|
||||||
|
if let Some(session_id) = self.session_id.clone() {
|
||||||
|
let field = screen.composer.field;
|
||||||
|
rsc.register_event(field, Submit, move |ctx, rsc| {
|
||||||
|
let text = field.edit(rsc).take();
|
||||||
|
let text = text.trim().to_string();
|
||||||
|
if !text.is_empty() {
|
||||||
|
ctx.state.send_message(session_id.clone(), text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.android_state_mut().set_root(tree);
|
||||||
|
self.screen = Some(screen);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_event(&mut self, rsc: &mut AndroidRsc<Self>, event: &SeqEvent) {
|
||||||
|
self.items = fold_event(&self.items, event);
|
||||||
|
self.rebuild_transcript(rsc);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send_message(&mut self, session_id: String, text: String) {
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
if let Ok(transport) = build_transport() {
|
||||||
|
let api = ApiClient::new(transport);
|
||||||
|
let _ = api.send_message(&session_id, &text, &[]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -72,6 +72,18 @@ impl<Rsc: HasState> Tasks<Rsc> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The same redraw handle `spawn`'s wrapper calls once, after a whole
|
||||||
|
/// task's future completes -- exposed so a caller running its own
|
||||||
|
/// longer-lived loop *inside* a spawned task (a live SSE follow, here)
|
||||||
|
/// can ask for a frame after each `TaskCtx::update`, not just at the
|
||||||
|
/// end. Without this a caller has no way to get a redraw mid-stream,
|
||||||
|
/// which is exactly the gap `iris/desktop-app`'s `app.rs` module doc
|
||||||
|
/// names for why it uses winit's `Proxy` instead of `Tasks` -- Android
|
||||||
|
/// has no `Proxy`, so this is what closes the same gap there.
|
||||||
|
pub fn redraw_handle(&self) -> Arc<dyn RequestRedraw> {
|
||||||
|
self.redraw.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
|
pub fn spawn<F: AsyncFnOnce(TaskCtx<Rsc>) + 'static + std::marker::Send>(&mut self, task: F)
|
||||||
where
|
where
|
||||||
F::CallOnceFuture: Send,
|
F::CallOnceFuture: Send,
|
||||||
|
|||||||
Reference in new issue
Block a user