feat: moved a2ui surface to a separate panel
This commit is contained in:
11
src/app.rs
11
src/app.rs
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user