//! I5's desktop proof: the transcript screen built from synthetic //! `ai_app::client::transcript_fold` rows (no network, no server -- see //! `lib.rs`'s doc for why `transcript-ui` itself never fetches anything), //! run via `iris/run-headless.sh transcript -- -p transcript-ui` for a //! screenshot on the winit backend, or `cargo run --example transcript -p //! transcript-ui` with a real compositor. //! //! The rows exercise every one of the seven "hard to get back" behaviours //! this box's markdown/selection work is meant to show: a heading, bold, //! italic, an inline code span, a link, a fenced code block (rich inline //! text), a multi-message conversation (bottom-anchored virtualised list), //! and a three-call tool run (collapsed by default -- tap it, or drive it //! with `ui-trace record --do "tap 'Tools'"` on Android, to prove //! hold-the-edge expand). use ai_app::client::QuestionOption; use ai_app::client::transcript_fold::{QuestionCard, TranscriptItem, TranscriptRow as FoldedRow}; use iris::prelude::*; fn main() { DefaultApp::::run(); } #[derive(DefaultUiState)] pub struct Client { ui_state: DefaultUiState, #[allow(dead_code)] screen: ai_app::ui::TranscriptScreen, } fn msg(seq: u64, from_user: bool, text: &str) -> FoldedRow { FoldedRow::Single(if from_user { TranscriptItem::UserMsg { seq, text: text.to_string(), attachments: Vec::new(), } } else { TranscriptItem::AssistantMsg { seq, text: text.to_string(), settled: true, } }) } /// One tool call. `result` is `None` for a call with no result yet and /// `Some((output, failed))` for one that answered. fn tool_call(id: &str, tool: &str, input: &str, result: Option<(&str, bool)>) -> TranscriptItem { tool_call_in("run1", id, tool, input, result) } /// The same, in a named run. Two runs in one transcript must not share a /// `run_id`: it is the row's identity in the list (`row::row_key`), and /// two rows under one key is the duplicate-key fault AGENTS.md's /// "Importing" section describes. Here it made two rows swap cached /// heights and draw at each other's boxes. fn tool_call_in( run: &str, id: &str, tool: &str, input: &str, result: Option<(&str, bool)>, ) -> TranscriptItem { TranscriptItem::ToolRun { seq: 3, id: id.into(), run_id: run.into(), tool: tool.into(), input: input.into(), output: result.map(|(out, _)| out.to_string()).unwrap_or_default(), done: result.is_some(), failed: result.is_some_and(|(_, failed)| failed), asks: Vec::new(), images: Vec::new(), } } /// A call stopped on the reader: one unanswered permission question. fn asking(id: &str, tool: &str, input: &str) -> TranscriptItem { let mut call = tool_call_in("run2", id, tool, input, None); if let TranscriptItem::ToolRun { asks, .. } = &mut call { asks.push(QuestionCard { seq: 9, id: format!("{id}-q"), prompt: "Allow this command?".into(), header: None, options: vec![ QuestionOption { label: "Allow".into(), description: None, preview: None, }, QuestionOption { label: "Deny".into(), description: None, preview: None, }, ], multi_select: false, answers: Vec::new(), }); } call } /// Longer than the card's own cap, so the "Show all N lines" control is on /// screen in the expanded shot. fn long_output() -> String { (0..200) .map(|i| format!("test ai_app::ui::case_{i} ... ok")) .collect::>() .join("\n") } fn synthetic_rows() -> Vec { vec![ msg( 1, true, "Can you show me a **bold** word, some *italic* text, and `inline code`?", ), msg( 2, false, "# Sure\n\nHere's a [link to the repo](https://example.com/ai-app-2) and a fenced block:\n\n```rust\nfn main() {\n println!(\"hi\");\n}\n```", ), // Every state a tool card has to draw, in one run (P1b): a call // that worked, one the tool reported as failed, one whose result // never arrived, and one still running. The last two look the same // in the events -- an empty output and `done: false` -- and are // told apart only by whether the session is still working, which // is what `TranscriptScreen::set_session_working` says. FoldedRow::Tools(vec![ tool_call( "t1", "Read", r#"{"file_path": "src/main.rs"}"#, Some(("fn main() {}\n", false)), ), tool_call( "t2", "Bash", r#"{"command": "cargo build --release", "timeout": 480000, "description": "Build it"}"#, Some(( "error: could not compile `iris`\nCaused by: linker not found", true, )), ), tool_call("t3", "Grep", r#"{"pattern": "fn fold_event"}"#, None), ]), // A lone call is a card too rather than a group of one -- and this // one carries the kilobyte output a collapsed card must not lay // out. FoldedRow::Single(tool_call( "t5", "Bash", r#"{"command": "cargo test -p transcript-ui -- --nocapture"}"#, Some((&long_output(), false)), )), msg(6, true, "Looks good, thanks!"), // Every block kind `ai_app::client::markdown_blocks` names, in one // row, so P1a's appearance can be looked at against the Compose // app's without a server (docs/RUST.md's P1a box). The heading, // paragraph, fence and table are the *same source* the bench // fixture carries (`app/bench-fixture/generate.py`), so the two // screenshots differ only in the renderer; the list and the quote // are extra, because the fixture has neither. msg(7, false, BLOCK_SAMPLER), ] } /// One of each markdown block, for the P1a screenshot pair. See /// [`synthetic_rows`]. const BLOCK_SAMPLER: &str = "\ ## What changed Iris **fold** render measure session window anchor context transcript \ iris measure iris scroll call transcript layout *cursor* context, and a \ [bench](https://example.com/bench) link. ```rust fn fold_event(items: Vec, seq: u64) -> Vec { // a comment worth keeping: this is the fold the app's own screen runs let mut out = items; out.push(Item::new(seq)); out } ``` | column | value | |---|---| | a | measure place draw tool call token context window anchor | - one bullet - another, with `inline code` - nested one level 1. first numbered 2. second numbered > A quoted line, to show the bar and the indent. "; impl DefaultAppState for Client { fn new( mut ui_state: DefaultUiState, rsc: &mut DefaultRsc, _: Proxy, ) -> Self { let screen = ai_app::ui::build(rsc, &mut ui_state, synthetic_rows()); // Exercises `push_row`/`ItemKey` beyond construction time, matching // how a live SSE loop appends -- a row arriving after the screen // already exists must land at the bottom without disturbing what's // above it (I3's `push_back`/`snap_end`). screen.push_row( rsc, &FoldedRow::Single(TranscriptItem::CommandRow { seq: 8, text: "clear".into(), }), ); // A second run at the live end, so the *running* state is on // screen too. It cannot share a row with "no result": the two are // the same events and are told apart only by whether the session // is working, which is a property of the row rather than of the // call (`TranscriptScreen::set_session_working`). screen.push_row( rsc, &FoldedRow::Tools(vec![ tool_call_in( "run2", "t6", "Read", r#"{"file_path": "docs/RUST.md"}"#, Some(("# Moving the app to Rust\n", false)), ), tool_call_in( "run2", "t7", "Bash", r#"{"command": "cargo clippy --workspace --all-targets"}"#, Some(("error: unused variable `x`", true)), ), tool_call_in("run2", "t8", "Glob", r#"{"pattern": "**/*.rs"}"#, None), // Waiting on a permission, so this card is drawn *open* // whatever the reader last chose -- the command is the // thing being decided, and a row saying only "Bash" // cannot be decided on. It is also how the expanded card // (input block, output block, timeout) gets into the // screenshot without a finger. asking( "t9", "Bash", r#"{"command": "rm -rf target", "timeout": 120000, "description": "Clear the build"}"#, ), ]), ); screen.set_session_working(rsc, true); // The expanded picture has no other way to be looked at on a // machine with no display and no finger -- see `run-headless.sh` // and docs/RUST.md's P1b box. if std::env::var_os("IRIS_TOOLS_EXPANDED").is_some() { assert!( screen.expand_tail_tools(rsc, true), "the newest row must be the tool run this flag is about" ); } Self { ui_state, screen } } }