prev/next A2UI surface and better dismissal

This commit is contained in:
Georg Bauer
2026-07-27 11:52:15 +02:00
parent c9c2d8efd5
commit 4a309091b6
14 changed files with 466 additions and 61 deletions

View File

@@ -79,6 +79,8 @@ pub(crate) struct App {
pub(super) queued_inputs: VecDeque<String>,
pub(super) conversation: Vec<ChatMessage>,
pub(super) a2ui: crate::a2ui::Store,
pub(super) a2ui_history: Vec<crate::a2ui::Store>,
pub(super) a2ui_history_index: Option<usize>,
pub(super) a2ui_tabs: HashMap<(String, String), usize>,
pub(super) a2ui_modals: HashSet<(String, String)>,
pub(super) a2ui_editors: HashMap<(String, String, String), text_editor::Content>,
@@ -87,6 +89,7 @@ pub(crate) struct App {
pub(super) a2ui_images: HashMap<String, iced::widget::image::Handle>,
pub(super) a2ui_image_requests: HashSet<String>,
pub(super) a2ui_image_loading: bool,
pub(super) pending_a2ui_dismissal: Option<String>,
pub(super) generating: bool,
pub(super) context_used: u32,
pub(super) context_limit: u32,
@@ -217,6 +220,10 @@ pub(crate) enum Message {
A2uiToggleModal(String, String),
A2uiImageLoaded(String, Result<Vec<u8>, String>),
A2uiPlayMedia(String, String, bool),
RequestA2uiDismiss(String),
ConfirmA2uiDismiss,
A2uiPreviousSurface,
A2uiNextSurface,
ResetPreferences,
SavePreferences,
DownloadArtifact(ManagedArtifactId),
@@ -328,6 +335,8 @@ impl App {
queued_inputs: VecDeque::new(),
conversation: Vec::new(),
a2ui: crate::a2ui::Store::default(),
a2ui_history: Vec::new(),
a2ui_history_index: None,
a2ui_tabs: HashMap::new(),
a2ui_modals: HashSet::new(),
a2ui_editors: HashMap::new(),
@@ -336,6 +345,7 @@ impl App {
a2ui_images: HashMap::new(),
a2ui_image_requests: HashSet::new(),
a2ui_image_loading: false,
pending_a2ui_dismissal: None,
generating: false,
context_used: 0,
context_limit,
@@ -443,6 +453,8 @@ impl App {
queued_inputs: VecDeque::new(),
conversation: Vec::new(),
a2ui: crate::a2ui::Store::default(),
a2ui_history: Vec::new(),
a2ui_history_index: None,
a2ui_tabs: HashMap::new(),
a2ui_modals: HashSet::new(),
a2ui_editors: HashMap::new(),
@@ -451,6 +463,7 @@ impl App {
a2ui_images: HashMap::new(),
a2ui_image_requests: HashSet::new(),
a2ui_image_loading: false,
pending_a2ui_dismissal: None,
generating: false,
context_used: 0,
context_limit,
@@ -538,7 +551,9 @@ impl App {
}
}
Message::DismissPanel => {
if self.session_rename.is_some() || self.session_menu.is_some() {
if self.pending_a2ui_dismissal.is_some() {
self.pending_a2ui_dismissal = None;
} else if self.session_rename.is_some() || self.session_menu.is_some() {
self.session_rename = None;
self.session_menu = None;
} else if self.preferences_open {
@@ -843,9 +858,15 @@ impl App {
Message::DownloadProgressTick => self.update_download_progress(),
Message::ComposerChanged(value) => self.composer = value,
Message::A2uiDataChanged(surface_id, path, value) => {
if self.a2ui_history_index.is_some() {
return Task::none();
}
return self.change_a2ui_data(surface_id, path, value);
}
Message::A2uiEditorAction(surface_id, component_id, path, action) => {
if self.a2ui_history_index.is_some() {
return Task::none();
}
let key = (surface_id.clone(), component_id, path.clone());
let Some(editor) = self.a2ui_editors.get_mut(&key) else {
return Task::none();
@@ -859,6 +880,9 @@ impl App {
.insert((surface_id, component_id, context_path), value);
}
Message::A2uiAction(surface_id, component_id, context_path) => {
if self.a2ui_history_index.is_some() {
return Task::none();
}
match self
.a2ui
.action(&surface_id, &component_id, context_path.as_deref())
@@ -907,6 +931,68 @@ impl App {
self.error = Some(format!("Could not open media: {error}"));
}
}
Message::RequestA2uiDismiss(surface_id) => {
if self.generating {
self.error = Some(
"Stop the active generation before dismissing its A2UI surface.".into(),
);
} else if self.a2ui_history_index.is_none()
&& self.a2ui.surface(&surface_id).is_some()
{
self.pending_a2ui_dismissal = Some(surface_id);
}
}
Message::ConfirmA2uiDismiss => {
let Some(surface_id) = self.pending_a2ui_dismissal.clone() else {
return Task::none();
};
let Some(session_id) = self.selected_session else {
self.error = Some("The A2UI surface is not attached to a saved chat.".into());
return Task::none();
};
let Some(database) = &mut self.database else {
self.error = Some("The project database is unavailable.".into());
return Task::none();
};
match database.dismiss_a2ui_surface(session_id, &surface_id) {
Ok(message) => {
self.conversation.push(ChatMessage::from(message));
self.a2ui_history.push(self.a2ui.clone());
self.a2ui.clear();
self.a2ui_history_index = None;
self.pending_a2ui_dismissal = None;
self.a2ui_tabs.clear();
self.a2ui_modals.clear();
self.sync_a2ui_renderer_state();
self.error = None;
}
Err(error) => {
self.error = Some(format!("Could not dismiss A2UI surface: {error}"));
}
}
}
Message::A2uiPreviousSurface => {
if !self.a2ui_history.is_empty() {
self.a2ui_history_index = Some(
self.a2ui_history_index
.map_or(self.a2ui_history.len() - 1, |index| index.saturating_sub(1)),
);
self.a2ui_tabs.clear();
self.a2ui_modals.clear();
self.sync_a2ui_renderer_state();
return self.load_next_a2ui_image();
}
}
Message::A2uiNextSurface => {
if let Some(index) = self.a2ui_history_index {
self.a2ui_history_index =
(index + 1 < self.a2ui_history.len()).then_some(index + 1);
self.a2ui_tabs.clear();
self.a2ui_modals.clear();
self.sync_a2ui_renderer_state();
return self.load_next_a2ui_image();
}
}
Message::ToggleReasoning(index) => {
if let Some(message) = self.conversation.get_mut(index)
&& message.reasoning.is_some()
@@ -1220,16 +1306,20 @@ impl App {
Ok((messages, a2ui)) => {
self.conversation = messages.into_iter().map(ChatMessage::from).collect();
self.clear_a2ui();
for message in a2ui {
if let Err(error) =
self.a2ui.apply_raw(&message.json, message.message_id)
{
self.error = Some(format!(
"Could not restore A2UI message {}: {error}",
message.id
));
}
}
let (history, active, errors) =
crate::a2ui::replay_epochs(a2ui.iter().map(|message| {
(
message.id,
message.message_id,
message.dismissed,
message.json.as_str(),
)
}));
self.a2ui_history = history;
self.a2ui = active;
let restore_error = (!errors.is_empty()).then(|| {
format!("Could not restore some A2UI history: {}", errors.join("; "))
});
self.sync_a2ui_renderer_state();
self.composer.clear();
self.remember_project(project_id);
@@ -1244,7 +1334,7 @@ impl App {
self.config.generation.context_tokens.max(0) as u32
};
self.tokens_per_second = tokens_per_second;
self.error = None;
self.error = restore_error;
return Task::batch([scroll_chat_to_end(), self.load_next_a2ui_image()]);
}
Err(error) => {