iris is the framework alone; the app is one crate in app-rust/

Iris: "the organization of the rust rewrite is a mess right now... there
shouldn't be anything related to the app inside of iris. Iris is supposed
to be the UI framework alone." And, on the crate count: "I'm confused why
the app only code needs more than one crate though."

Nine cargo workspaces become three, and the port's project code -- which
sat in five places, four of them inside the framework -- becomes one crate,
`ai-app`, in `app-rust/`:

  client-core                -> app-rust/src/client
  iris/transcript-ui         -> app-rust/src/ui
  iris/transcript-fixture    -> app-rust/src/ui/fixture.rs + tests/ + touch/
  iris/desktop-app           -> app-rust/src/desktop + src/bin_desktop.rs
  iris/android-app           -> app-rust/src/android + android-project/
  android-shell              -> app-rust/src/shell

iris/ keeps core, macro, the iris crate, tabs-ui and rig-input, and now
mentions no session, transcript, setup or server anywhere.

Only two of the old splits had a reason that survived reading. event-model
stays a crate at the repo root because server/ depends on it too, so a
crate is what makes the backend and the app agree by construction. The two
Android .so names looked like a hard constraint -- a package produces one
library artifact -- until P2 turned out to already plan merging those two
Android apps into one; both faces now come out of libai_app.so, picked
apart by features so `--no-default-features --features shell` keeps wgpu,
parley and iris out of the Compose app's APK. docs/RUST.md's "One app
crate" has the rest, including what each remaining feature is for.

DECISIONS.md and SUBAGENTS.md move into docs/ with everything else.

Verified: ./run-tests.sh and `cd iris && cargo test` green, clippy and fmt
clean in all five workspaces, `cargo ndk -t x86_64` links libai_app.so,
build-apk.sh produces an APK that installs and launches on this checkout's
emulator (Gl ... virgl, as expected), and the phone-sized headless
screenshot renders the transcript unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
irisandClaude Opus 5 committed 2026-09-08 23:36:38 -04:00
1 parent 7b54aaf3c4
commit a9312e9431
113 files changed
+23221 -2992

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
use crate::platform::OpenUrl;
use android_view::{
View,
jni::{JNIEnv, objects::JValue},
};
use super::view::HasAndroidUiState;
/// Android's URL opener. Like `FocusHost::focus_gained`'s keyboard, the
/// real work is a JNI call and this runs deep inside the sensor dispatch
/// with no `CallbackCtx` in reach -- so it raises a flag that
/// `IrisViewPeer::after_input` consumes, exactly as
/// `pending_show_keyboard` does.
///
/// Last request wins: two links cannot be tapped in one frame, and a URL
/// left queued from a frame that somehow never reached `after_input`
/// would open at some unrelated later tap, which is worse than dropping
/// it.
impl<T: HasAndroidUiState> OpenUrl for T {
fn open_url(&mut self, url: &str) {
self.android_state_mut().pending_open_url = Some(url.to_string());
}
}
/// `startActivity(new Intent(ACTION_VIEW, Uri.parse(url)))` on the view's
/// own context.
///
/// `FLAG_ACTIVITY_NEW_TASK` because the context here is the view's, which
/// may be an application context rather than the activity's -- Android
/// throws `AndroidRuntimeException` for a non-activity context without it,
/// and it is harmless when the context *is* an activity's.
///
/// Every failure is logged with the URL and returns; there is nothing to
/// fall back to, and the reader will see that nothing happened.
pub(super) fn open_url<'local>(env: &mut JNIEnv<'local>, view: &View<'local>, url: &str) {
match try_open_url(env, view, url) {
Ok(()) => {}
Err(e) => {
// A pending Java exception makes every later JNI call fail in
// ways nowhere near here, so it is cleared at the boundary.
let _ = env.exception_clear();
log::warn!("could not open {url}: {e}");
}
}
}
fn try_open_url<'local>(
env: &mut JNIEnv<'local>,
view: &View<'local>,
url: &str,
) -> Result<(), android_view::jni::errors::Error> {
let context = env
.call_method(&view.0, "getContext", "()Landroid/content/Context;", &[])?
.l()?;
let jurl = env.new_string(url)?;
let uri = env.call_static_method(
"android/net/Uri",
"parse",
"(Ljava/lang/String;)Landroid/net/Uri;",
&[JValue::Object(jurl.as_ref())],
)?;
let action = env.new_string("android.intent.action.VIEW")?;
let intent = env.new_object(
"android/content/Intent",
"(Ljava/lang/String;Landroid/net/Uri;)V",
&[JValue::Object(action.as_ref()), JValue::Object(&uri.l()?)],
)?;
env.call_method(
&intent,
"addFlags",
"(I)Landroid/content/Intent;",
&[JValue::Int(FLAG_ACTIVITY_NEW_TASK)],
)?;
env.call_method(
&context,
"startActivity",
"(Landroid/content/Intent;)V",
&[JValue::Object(&intent)],
)?;
Ok(())
}
/// `android.content.Intent.FLAG_ACTIVITY_NEW_TASK`. A constant rather than
/// a static-field read: it is part of the platform's stable ABI and
/// reading it costs two more JNI calls that can each fail.
const FLAG_ACTIVITY_NEW_TASK: i32 = 0x1000_0000;