feat: moved a2ui surface to a separate panel

This commit is contained in:
Georg Bauer
2026-07-27 12:58:46 +02:00
parent ef1c574a7f
commit f69623ad02
5 changed files with 93 additions and 17 deletions

View File

@@ -90,6 +90,7 @@ pub(crate) struct App {
pub(super) a2ui_image_requests: HashSet<String>,
pub(super) a2ui_image_loading: bool,
pub(super) pending_a2ui_dismissal: Option<String>,
pub(super) a2ui_auto_switch_pending: bool,
pub(super) generating: bool,
pub(super) context_used: u32,
pub(super) context_limit: u32,
@@ -141,6 +142,7 @@ pub(crate) struct App {
pub(super) enum DetailTab {
#[default]
Chat,
A2ui,
Stats,
}
@@ -264,6 +266,7 @@ pub(crate) enum Message {
SetSessionState(i32, SessionState),
ToggleArchivedSessions(i32),
ShowChat,
ShowA2ui,
ShowStats,
ToggleSidebar,
StartSidebarDrag,
@@ -346,6 +349,7 @@ impl App {
a2ui_image_requests: HashSet::new(),
a2ui_image_loading: false,
pending_a2ui_dismissal: None,
a2ui_auto_switch_pending: false,
generating: false,
context_used: 0,
context_limit,
@@ -464,6 +468,7 @@ impl App {
a2ui_image_requests: HashSet::new(),
a2ui_image_loading: false,
pending_a2ui_dismissal: None,
a2ui_auto_switch_pending: false,
generating: false,
context_used: 0,
context_limit,
@@ -571,7 +576,11 @@ impl App {
Message::FocusPrevious => {
return iced::widget::operation::focus_previous().chain(reveal_focused());
}
Message::ShowChat => self.detail_tab = DetailTab::Chat,
Message::ShowChat => {
self.detail_tab = DetailTab::Chat;
return scroll_chat_to_end();
}
Message::ShowA2ui => self.detail_tab = DetailTab::A2ui,
Message::ShowStats => {
self.detail_tab = DetailTab::Stats;
self.scan_kv_cache();

View File

@@ -149,8 +149,9 @@ fn sync_a2ui_message(
database: &mut Option<Database>,
session_id: Option<i32>,
message: &mut ChatMessage,
) {
) -> bool {
let lines = crate::a2ui::extract_lines(&message.content);
let mut renderable_surface_updated = false;
for (index, line) in lines.iter().enumerate().skip(message.a2ui_lines_processed) {
let applied = match &line.value {
Ok(value) => store.apply(value.clone(), line.raw.clone(), message.id),
@@ -158,6 +159,13 @@ fn sync_a2ui_message(
};
match applied {
Ok(applied) => {
renderable_surface_updated |= line.value.as_ref().is_ok_and(|value| {
crate::a2ui::message_surface_id(value).is_some_and(|id| {
store.active_surface().is_some_and(|surface| {
surface.id == id && surface.components.contains_key("root")
})
})
});
if let Some(reply) = applied.reply {
message.a2ui_replies.push(reply);
}
@@ -185,6 +193,7 @@ fn sync_a2ui_message(
}
message.a2ui_lines_processed = lines.len();
message.refresh_markdown();
renderable_surface_updated
}
#[cfg(target_os = "macos")]
@@ -300,6 +309,7 @@ impl App {
));
return;
}
self.a2ui_auto_switch_pending = true;
#[cfg(target_os = "macos")]
self.tool_cards.clear();
#[cfg(target_os = "macos")]
@@ -617,13 +627,17 @@ impl App {
&& self.config.a2ui_enabled
&& let Some(message) = self.conversation.last_mut()
{
sync_a2ui_message(
let renderable_surface_updated = sync_a2ui_message(
&mut self.a2ui,
&mut self.database,
self.selected_session,
message,
);
a2ui_changed = true;
if self.a2ui_auto_switch_pending && renderable_surface_updated {
self.detail_tab = DetailTab::A2ui;
self.a2ui_auto_switch_pending = false;
}
}
}
Ok(GenerationEvent::Context {
@@ -943,6 +957,9 @@ impl App {
);
let assistant_reasoning = effective.turn.reasoning_mode != ReasoningMode::Direct;
let queued = queued_prompt(self.queued_inputs.drain(..));
if queued.is_some() {
self.a2ui_auto_switch_pending = true;
}
let reminders = if self.system_prompt_reminder_due() {
self.system_prompt_reminders(model)
} else {
@@ -1576,6 +1593,7 @@ mod tests {
use super::{
ChatMessage, TOOL_PROTOCOL_CORRECTION, compacted_context_start, correction_already_sent,
has_chat_after_last_compaction, has_misplaced_tool_call, is_empty_response, queued_prompt,
sync_a2ui_message,
};
use crate::model::ModelChoice;
@@ -1672,6 +1690,36 @@ mod tests {
assert_eq!(rows.len(), 2);
}
#[test]
fn a2ui_auto_switch_waits_for_a_renderable_surface() {
let mut store = crate::a2ui::Store::default();
let mut database = None;
let mut message = assistant(
None,
r#"```a2ui
{"version":"v1.0","createSurface":{"surfaceId":"answer","catalogId":"https://ds4server.local/a2ui/v1_0/catalog.json"}}
```"#,
);
assert!(!sync_a2ui_message(
&mut store,
&mut database,
None,
&mut message
));
message.content = r#"```a2ui
{"version":"v1.0","createSurface":{"surfaceId":"answer","catalogId":"https://ds4server.local/a2ui/v1_0/catalog.json"}}
{"version":"v1.0","updateComponents":{"surfaceId":"answer","components":[{"id":"root","component":"Text","text":"Ready"}]}}
```"#
.to_owned();
assert!(sync_a2ui_message(
&mut store,
&mut database,
None,
&mut message
));
}
#[test]
fn last_compaction_selects_its_tail_without_hiding_history() {
let message = |id: i32, compaction: bool, tail: Option<i32>| ChatMessage {

View File

@@ -431,6 +431,7 @@ impl App {
fn detail_tabs(&self) -> Element<'_, Message> {
let chat_active = self.detail_tab == DetailTab::Chat;
let a2ui_active = self.detail_tab == DetailTab::A2ui;
let stats_active = self.detail_tab == DetailTab::Stats;
let tabs = container(
row![
@@ -440,6 +441,12 @@ impl App {
.padding([0, 16])
.on_press(Message::ShowChat)
.style(move |theme, status| segmented_button_style(theme, status, chat_active)),
button(text("A2UI").size(13))
.width(88)
.height(TITLE_BAR_CONTROL - 4.0)
.padding([0, 16])
.on_press(Message::ShowA2ui)
.style(move |theme, status| segmented_button_style(theme, status, a2ui_active)),
button(text("Stats").size(13))
.width(88)
.height(TITLE_BAR_CONTROL - 4.0)
@@ -461,6 +468,7 @@ impl App {
fn detail(&self) -> Element<'_, Message> {
match self.detail_tab {
DetailTab::Chat => self.chat_detail(),
DetailTab::A2ui => self.a2ui_detail(),
DetailTab::Stats => self.stats_dashboard(),
}
}

View File

@@ -6,6 +6,30 @@ use std::collections::{BTreeSet, HashMap, HashSet};
use time::{Month, OffsetDateTime};
impl App {
pub(super) fn a2ui_detail(&self) -> Element<'_, Message> {
let content = self.displayed_a2ui_surface().unwrap_or_else(|| {
container(
column![
text("No A2UI surface").size(24),
text("Interactive surfaces from DS4 will appear here.")
.size(14)
.color(muted_text()),
]
.spacing(8)
.align_x(Alignment::Center),
)
.center_x(Length::Fill)
.center_y(Length::Fill)
.into()
});
container(content)
.max_width(860)
.center_x(Length::Fill)
.height(Length::Fill)
.padding(24)
.into()
}
pub(crate) fn sync_a2ui_renderer_state(&mut self) {
let mut markdown = HashMap::new();
let mut editors = HashMap::new();

View File

@@ -293,21 +293,8 @@ impl App {
let transcript = scrollable(messages)
.id(chat_scroll_id())
.height(Length::Fill);
let workspace: Element<'_, Message> =
if let Some(surface) = self.displayed_a2ui_surface() {
column![
container(surface).height(Length::FillPortion(1)),
rule::horizontal(1),
container(transcript).height(Length::FillPortion(1)),
]
.height(Length::Fill)
.spacing(8)
.into()
} else {
transcript.into()
};
let conversation = column![
workspace,
transcript,
container(composer_content,)
.padding(16)
.width(Length::Fill)