1use crate::application::{UiApplication, UiApplicationPage};
2use crate::compat::{Result, Tool, ToolContext};
3use crate::schema::{Component, UiResponse};
4use crate::tools::{LegacyProtocolOptions, render_ui_response_with_protocol};
5use async_trait::async_trait;
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use serde_json::{Map, Value, json};
9use std::sync::Arc;
10
11#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
13pub struct RenderAppParams {
14 pub application: UiApplication,
15 #[serde(flatten)]
18 pub protocol: LegacyProtocolOptions,
19}
20
21pub struct RenderAppTool;
22
23impl RenderAppTool {
24 pub fn new() -> Self {
25 Self
26 }
27}
28
29impl Default for RenderAppTool {
30 fn default() -> Self {
31 Self::new()
32 }
33}
34
35fn page_components(page: &UiApplicationPage) -> Vec<Component> {
36 let mut components = Vec::new();
37 if let Some(hero) = &page.hero {
38 components.extend(hero.content.clone());
39 components.extend(hero.actions.clone());
40 components.extend(hero.visual.clone());
41 }
42 for region in &page.regions {
43 components.extend(region.components.clone());
44 }
45 components.extend(page.aside.clone());
46 components.extend(page.footer.clone());
47 components
48}
49
50fn add_application_fields(
51 primary: Value,
52 application: &UiApplication,
53 surfaces: Vec<Value>,
54) -> Result<Value> {
55 let mut object = match primary {
56 Value::Object(object) => object,
57 other => {
58 let mut object = Map::new();
59 object.insert("payload".to_string(), other);
60 object
61 }
62 };
63 object.insert(
64 "application".to_string(),
65 serde_json::to_value(application).map_err(|error| {
66 crate::compat::AdkError::tool(format!("Failed to serialize application: {error}"))
67 })?,
68 );
69 object.insert("surfaces".to_string(), Value::Array(surfaces));
70 object.insert(
71 "application_version".to_string(),
72 json!(application.version),
73 );
74 Ok(Value::Object(object))
75}
76
77#[async_trait]
78impl Tool for RenderAppTool {
79 fn name(&self) -> &str {
80 "render_app"
81 }
82
83 fn description(&self) -> &str {
84 "Render a complete multi-page application with brand-aware page composition, navigation, shared state, and optional declarative 3D scenes. Use this instead of render_page for products, sites, dashboards, portals, and end-to-end workflows with multiple routes."
85 }
86
87 fn parameters_schema(&self) -> Option<Value> {
88 Some(super::generate_gemini_schema::<RenderAppParams>())
89 }
90
91 async fn execute(&self, ctx: Arc<dyn ToolContext>, args: Value) -> Result<Value> {
92 let mut params: RenderAppParams = serde_json::from_value(args).map_err(|error| {
93 crate::compat::AdkError::tool(format!("Invalid parameters: {error}"))
94 })?;
95 params
96 .application
97 .normalize_metadata(crate::surface_runtime::surface_owner(ctx.as_ref()));
98 params.application.validate().map_err(|errors| {
99 crate::compat::AdkError::tool(format!("Invalid application: {}", errors.join("; ")))
100 })?;
101
102 let mut surfaces = Vec::with_capacity(params.application.pages.len());
103 for page in ¶ms.application.pages {
104 let mut options = params.protocol.clone();
105 options.surface_id = Some(format!("{}-{}", params.application.id, page.id));
106 if options.kit_id.is_none() {
107 options.kit_id = params.application.kit_id.clone();
108 }
109 let ui = UiResponse::new(page_components(page))
110 .with_id(format!("{}:{}", params.application.id, page.id));
111 let ui = match ¶ms.application.kit_id {
112 Some(kit_id) => ui.with_kit_id(kit_id),
113 None => ui,
114 };
115 surfaces.push(render_ui_response_with_protocol(ui, &options, &page.id)?);
116 }
117
118 let primary_index = params
119 .application
120 .pages
121 .iter()
122 .position(|page| page.route == params.application.initial_route)
123 .unwrap_or(0);
124 let primary = surfaces
125 .get(primary_index)
126 .cloned()
127 .unwrap_or_else(|| json!({}));
128 let output = if params.protocol.protocol.is_none() {
129 json!({
130 "application_version": params.application.version,
131 "application": ¶ms.application,
132 "surfaces": surfaces,
133 })
134 } else {
135 add_application_fields(primary, ¶ms.application, surfaces)?
136 };
137
138 let refs = params
139 .application
140 .pages
141 .iter()
142 .map(|page| {
143 crate::surface_runtime::next_surface_ref(
144 &ctx,
145 format!("{}-{}", params.application.id, page.id),
146 )
147 })
148 .collect::<Vec<_>>();
149 if let Some(active) = refs.get(primary_index) {
150 crate::surface_runtime::record_surface_refs(&ctx, active, &refs);
151 }
152 Ok(output)
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::application::*;
160 use crate::compat::{Content, EventActions, ReadonlyContext};
161 use crate::interop::UiProtocol;
162 use async_trait::async_trait;
163 use std::collections::HashMap;
164 use std::sync::Mutex;
165
166 struct TestContext {
167 content: Content,
168 actions: Mutex<EventActions>,
169 }
170 #[async_trait]
171 impl ReadonlyContext for TestContext {
172 fn invocation_id(&self) -> &str {
173 "test"
174 }
175 fn agent_name(&self) -> &str {
176 "test"
177 }
178 fn user_id(&self) -> &str {
179 "user"
180 }
181 fn app_name(&self) -> &str {
182 "app"
183 }
184 fn session_id(&self) -> &str {
185 "session"
186 }
187 fn branch(&self) -> &str {
188 ""
189 }
190 fn user_content(&self) -> &Content {
191 &self.content
192 }
193 }
194 #[async_trait]
195 impl crate::compat::CallbackContext for TestContext {
196 fn artifacts(&self) -> Option<Arc<dyn crate::compat::Artifacts>> {
197 None
198 }
199 }
200 #[async_trait]
201 impl ToolContext for TestContext {
202 fn function_call_id(&self) -> &str {
203 "call"
204 }
205 fn actions(&self) -> EventActions {
206 self.actions.lock().unwrap().clone()
207 }
208 fn set_actions(&self, actions: EventActions) {
209 *self.actions.lock().unwrap() = actions;
210 }
211 async fn search_memory(
212 &self,
213 _: &str,
214 ) -> crate::compat::Result<Vec<crate::compat::MemoryEntry>> {
215 Ok(vec![])
216 }
217 }
218
219 fn application() -> UiApplication {
220 UiApplication {
221 id: "app".into(),
222 name: "App".into(),
223 version: 1,
224 owner: "test".into(),
225 updated_at: chrono::Utc::now().to_rfc3339(),
226 kit_id: None,
227 initial_route: "/".into(),
228 navigation: vec![],
229 state: HashMap::new(),
230 pages: vec![UiApplicationPage {
231 id: "home".into(),
232 route: "/".into(),
233 title: "Home".into(),
234 description: None,
235 template: UiPageTemplate::Dashboard,
236 eyebrow: None,
237 hero: None,
238 regions: vec![UiPageRegion {
239 id: "main".into(),
240 title: None,
241 description: None,
242 layout: UiRegionLayout::Stack,
243 columns: 1,
244 tone: UiRegionTone::Default,
245 components: vec![crate::schema::Component::Text(crate::schema::Text {
246 id: None,
247 content: "Hello".into(),
248 variant: crate::schema::TextVariant::Body,
249 })],
250 }],
251 aside: vec![],
252 footer: vec![],
253 atmosphere: UiPageAtmosphere::Clean,
254 }],
255 }
256 }
257
258 #[tokio::test]
259 async fn emits_application_and_protocol_fallback_surfaces() {
260 let args = serde_json::to_value(RenderAppParams {
261 application: application(),
262 protocol: LegacyProtocolOptions {
263 protocol: Some(UiProtocol::A2ui),
264 ..Default::default()
265 },
266 })
267 .unwrap();
268 let ctx: Arc<dyn ToolContext> = Arc::new(TestContext {
269 content: Content::new("user"),
270 actions: Mutex::new(EventActions::default()),
271 });
272 let value = RenderAppTool.execute(ctx, args).await.unwrap();
273 assert_eq!(value["protocol"], "a2ui");
274 assert_eq!(value["application"]["initial_route"], "/");
275 assert_eq!(value["surfaces"].as_array().unwrap().len(), 1);
276 }
277}