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_requests: HashSet<String>,
|
||||||
pub(super) a2ui_image_loading: bool,
|
pub(super) a2ui_image_loading: bool,
|
||||||
pub(super) pending_a2ui_dismissal: Option<String>,
|
pub(super) pending_a2ui_dismissal: Option<String>,
|
||||||
|
pub(super) a2ui_auto_switch_pending: bool,
|
||||||
pub(super) generating: bool,
|
pub(super) generating: bool,
|
||||||
pub(super) context_used: u32,
|
pub(super) context_used: u32,
|
||||||
pub(super) context_limit: u32,
|
pub(super) context_limit: u32,
|
||||||
@@ -141,6 +142,7 @@ pub(crate) struct App {
|
|||||||
pub(super) enum DetailTab {
|
pub(super) enum DetailTab {
|
||||||
#[default]
|
#[default]
|
||||||
Chat,
|
Chat,
|
||||||
|
A2ui,
|
||||||
Stats,
|
Stats,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,6 +266,7 @@ pub(crate) enum Message {
|
|||||||
SetSessionState(i32, SessionState),
|
SetSessionState(i32, SessionState),
|
||||||
ToggleArchivedSessions(i32),
|
ToggleArchivedSessions(i32),
|
||||||
ShowChat,
|
ShowChat,
|
||||||
|
ShowA2ui,
|
||||||
ShowStats,
|
ShowStats,
|
||||||
ToggleSidebar,
|
ToggleSidebar,
|
||||||
StartSidebarDrag,
|
StartSidebarDrag,
|
||||||
@@ -346,6 +349,7 @@ impl App {
|
|||||||
a2ui_image_requests: HashSet::new(),
|
a2ui_image_requests: HashSet::new(),
|
||||||
a2ui_image_loading: false,
|
a2ui_image_loading: false,
|
||||||
pending_a2ui_dismissal: None,
|
pending_a2ui_dismissal: None,
|
||||||
|
a2ui_auto_switch_pending: false,
|
||||||
generating: false,
|
generating: false,
|
||||||
context_used: 0,
|
context_used: 0,
|
||||||
context_limit,
|
context_limit,
|
||||||
@@ -464,6 +468,7 @@ impl App {
|
|||||||
a2ui_image_requests: HashSet::new(),
|
a2ui_image_requests: HashSet::new(),
|
||||||
a2ui_image_loading: false,
|
a2ui_image_loading: false,
|
||||||
pending_a2ui_dismissal: None,
|
pending_a2ui_dismissal: None,
|
||||||
|
a2ui_auto_switch_pending: false,
|
||||||
generating: false,
|
generating: false,
|
||||||
context_used: 0,
|
context_used: 0,
|
||||||
context_limit,
|
context_limit,
|
||||||
@@ -571,7 +576,11 @@ impl App {
|
|||||||
Message::FocusPrevious => {
|
Message::FocusPrevious => {
|
||||||
return iced::widget::operation::focus_previous().chain(reveal_focused());
|
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 => {
|
Message::ShowStats => {
|
||||||
self.detail_tab = DetailTab::Stats;
|
self.detail_tab = DetailTab::Stats;
|
||||||
self.scan_kv_cache();
|
self.scan_kv_cache();
|
||||||
|
|||||||
@@ -149,8 +149,9 @@ fn sync_a2ui_message(
|
|||||||
database: &mut Option<Database>,
|
database: &mut Option<Database>,
|
||||||
session_id: Option<i32>,
|
session_id: Option<i32>,
|
||||||
message: &mut ChatMessage,
|
message: &mut ChatMessage,
|
||||||
) {
|
) -> bool {
|
||||||
let lines = crate::a2ui::extract_lines(&message.content);
|
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) {
|
for (index, line) in lines.iter().enumerate().skip(message.a2ui_lines_processed) {
|
||||||
let applied = match &line.value {
|
let applied = match &line.value {
|
||||||
Ok(value) => store.apply(value.clone(), line.raw.clone(), message.id),
|
Ok(value) => store.apply(value.clone(), line.raw.clone(), message.id),
|
||||||
@@ -158,6 +159,13 @@ fn sync_a2ui_message(
|
|||||||
};
|
};
|
||||||
match applied {
|
match applied {
|
||||||
Ok(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 {
|
if let Some(reply) = applied.reply {
|
||||||
message.a2ui_replies.push(reply);
|
message.a2ui_replies.push(reply);
|
||||||
}
|
}
|
||||||
@@ -185,6 +193,7 @@ fn sync_a2ui_message(
|
|||||||
}
|
}
|
||||||
message.a2ui_lines_processed = lines.len();
|
message.a2ui_lines_processed = lines.len();
|
||||||
message.refresh_markdown();
|
message.refresh_markdown();
|
||||||
|
renderable_surface_updated
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -300,6 +309,7 @@ impl App {
|
|||||||
));
|
));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
self.a2ui_auto_switch_pending = true;
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
self.tool_cards.clear();
|
self.tool_cards.clear();
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
@@ -617,13 +627,17 @@ impl App {
|
|||||||
&& self.config.a2ui_enabled
|
&& self.config.a2ui_enabled
|
||||||
&& let Some(message) = self.conversation.last_mut()
|
&& let Some(message) = self.conversation.last_mut()
|
||||||
{
|
{
|
||||||
sync_a2ui_message(
|
let renderable_surface_updated = sync_a2ui_message(
|
||||||
&mut self.a2ui,
|
&mut self.a2ui,
|
||||||
&mut self.database,
|
&mut self.database,
|
||||||
self.selected_session,
|
self.selected_session,
|
||||||
message,
|
message,
|
||||||
);
|
);
|
||||||
a2ui_changed = true;
|
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 {
|
Ok(GenerationEvent::Context {
|
||||||
@@ -943,6 +957,9 @@ impl App {
|
|||||||
);
|
);
|
||||||
let assistant_reasoning = effective.turn.reasoning_mode != ReasoningMode::Direct;
|
let assistant_reasoning = effective.turn.reasoning_mode != ReasoningMode::Direct;
|
||||||
let queued = queued_prompt(self.queued_inputs.drain(..));
|
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() {
|
let reminders = if self.system_prompt_reminder_due() {
|
||||||
self.system_prompt_reminders(model)
|
self.system_prompt_reminders(model)
|
||||||
} else {
|
} else {
|
||||||
@@ -1576,6 +1593,7 @@ mod tests {
|
|||||||
use super::{
|
use super::{
|
||||||
ChatMessage, TOOL_PROTOCOL_CORRECTION, compacted_context_start, correction_already_sent,
|
ChatMessage, TOOL_PROTOCOL_CORRECTION, compacted_context_start, correction_already_sent,
|
||||||
has_chat_after_last_compaction, has_misplaced_tool_call, is_empty_response, queued_prompt,
|
has_chat_after_last_compaction, has_misplaced_tool_call, is_empty_response, queued_prompt,
|
||||||
|
sync_a2ui_message,
|
||||||
};
|
};
|
||||||
use crate::model::ModelChoice;
|
use crate::model::ModelChoice;
|
||||||
|
|
||||||
@@ -1672,6 +1690,36 @@ mod tests {
|
|||||||
assert_eq!(rows.len(), 2);
|
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]
|
#[test]
|
||||||
fn last_compaction_selects_its_tail_without_hiding_history() {
|
fn last_compaction_selects_its_tail_without_hiding_history() {
|
||||||
let message = |id: i32, compaction: bool, tail: Option<i32>| ChatMessage {
|
let message = |id: i32, compaction: bool, tail: Option<i32>| ChatMessage {
|
||||||
|
|||||||
@@ -431,6 +431,7 @@ impl App {
|
|||||||
|
|
||||||
fn detail_tabs(&self) -> Element<'_, Message> {
|
fn detail_tabs(&self) -> Element<'_, Message> {
|
||||||
let chat_active = self.detail_tab == DetailTab::Chat;
|
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 stats_active = self.detail_tab == DetailTab::Stats;
|
||||||
let tabs = container(
|
let tabs = container(
|
||||||
row![
|
row![
|
||||||
@@ -440,6 +441,12 @@ impl App {
|
|||||||
.padding([0, 16])
|
.padding([0, 16])
|
||||||
.on_press(Message::ShowChat)
|
.on_press(Message::ShowChat)
|
||||||
.style(move |theme, status| segmented_button_style(theme, status, chat_active)),
|
.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))
|
button(text("Stats").size(13))
|
||||||
.width(88)
|
.width(88)
|
||||||
.height(TITLE_BAR_CONTROL - 4.0)
|
.height(TITLE_BAR_CONTROL - 4.0)
|
||||||
@@ -461,6 +468,7 @@ impl App {
|
|||||||
fn detail(&self) -> Element<'_, Message> {
|
fn detail(&self) -> Element<'_, Message> {
|
||||||
match self.detail_tab {
|
match self.detail_tab {
|
||||||
DetailTab::Chat => self.chat_detail(),
|
DetailTab::Chat => self.chat_detail(),
|
||||||
|
DetailTab::A2ui => self.a2ui_detail(),
|
||||||
DetailTab::Stats => self.stats_dashboard(),
|
DetailTab::Stats => self.stats_dashboard(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,30 @@ use std::collections::{BTreeSet, HashMap, HashSet};
|
|||||||
use time::{Month, OffsetDateTime};
|
use time::{Month, OffsetDateTime};
|
||||||
|
|
||||||
impl App {
|
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) {
|
pub(crate) fn sync_a2ui_renderer_state(&mut self) {
|
||||||
let mut markdown = HashMap::new();
|
let mut markdown = HashMap::new();
|
||||||
let mut editors = HashMap::new();
|
let mut editors = HashMap::new();
|
||||||
|
|||||||
@@ -293,21 +293,8 @@ impl App {
|
|||||||
let transcript = scrollable(messages)
|
let transcript = scrollable(messages)
|
||||||
.id(chat_scroll_id())
|
.id(chat_scroll_id())
|
||||||
.height(Length::Fill);
|
.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![
|
let conversation = column![
|
||||||
workspace,
|
transcript,
|
||||||
container(composer_content,)
|
container(composer_content,)
|
||||||
.padding(16)
|
.padding(16)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
|||||||
Reference in New Issue
Block a user