feat: more native menus with expected reactions

This commit is contained in:
Georg Bauer
2026-07-27 15:09:12 +02:00
parent 8f02f0934b
commit c96675c462
11 changed files with 738 additions and 70 deletions

View File

@@ -61,6 +61,7 @@ pub(crate) struct ChatMessage {
pub(super) reasoning_open: bool,
pub(super) content: String,
pub(super) markdown: markdown::Content,
pub(super) transcript: text_editor::Content,
pub(super) a2ui_lines_processed: usize,
pub(super) a2ui_errors: Vec<String>,
pub(super) a2ui_replies: Vec<serde_json::Value>,
@@ -89,14 +90,17 @@ impl ChatMessage {
}
pub(super) fn refresh_markdown(&mut self) {
let visible = crate::agent::visible_content(&self.content);
let visible = crate::a2ui::transcript_fallback(visible);
let content = if self.reasoning.is_some() {
visible.trim_start()
} else {
&visible
};
if self.transcript.text() != content {
self.transcript = text_editor::Content::with_text(content);
}
if !self.user && !self.tool {
let visible = crate::agent::visible_content(&self.content);
let visible = crate::a2ui::transcript_fallback(visible);
let content = if self.reasoning.is_some() {
visible.trim_start()
} else {
&visible
};
self.markdown = markdown::Content::parse(content);
}
}
@@ -165,6 +169,7 @@ impl From<StoredMessage> for ChatMessage {
reasoning_open: false,
content: message.content,
markdown: iced::widget::markdown::Content::new(),
transcript: iced::widget::text_editor::Content::new(),
a2ui_lines_processed: 0,
a2ui_errors: Vec::new(),
a2ui_replies: Vec::new(),
@@ -341,6 +346,7 @@ impl App {
return;
}
self.a2ui_auto_switch_pending = true;
self.context_notice = None;
#[cfg(target_os = "macos")]
self.tool_cards.clear();
#[cfg(target_os = "macos")]
@@ -633,6 +639,9 @@ impl App {
match active.events.try_recv() {
Ok(GenerationEvent::Loading) => {}
Ok(GenerationEvent::Activity(activity)) => {
if activity.starts_with("Rebuilding context:") {
self.context_notice = Some(activity.to_owned());
}
self.activity = Some(activity.into());
}
Ok(GenerationEvent::Compacted(_)) => {
@@ -1666,6 +1675,7 @@ mod tests {
reasoning_open: false,
content: content.to_owned(),
markdown: iced::widget::markdown::Content::new(),
transcript: iced::widget::text_editor::Content::new(),
a2ui_lines_processed: 0,
a2ui_errors: Vec::new(),
a2ui_replies: Vec::new(),
@@ -1727,6 +1737,7 @@ mod tests {
content: "### Core / Setup\n\n| File | Lines |\n|---|---:|\n| `src/app.rs` | **1,750** |\n| `src/engine.rs` | 2,400 |\n\n### Summary\n\nDone."
.to_owned(),
markdown: iced::widget::markdown::Content::new(),
transcript: iced::widget::text_editor::Content::new(),
a2ui_lines_processed: 0,
a2ui_errors: Vec::new(),
a2ui_replies: Vec::new(),
@@ -1792,6 +1803,7 @@ mod tests {
reasoning_open: false,
content: format!("message {id}"),
markdown: iced::widget::markdown::Content::new(),
transcript: iced::widget::text_editor::Content::new(),
a2ui_lines_processed: 0,
a2ui_errors: Vec::new(),
a2ui_replies: Vec::new(),
@@ -1829,6 +1841,7 @@ mod tests {
reasoning_open: false,
content: format!("message {id}"),
markdown: iced::widget::markdown::Content::new(),
transcript: iced::widget::text_editor::Content::new(),
a2ui_lines_processed: 0,
a2ui_errors: Vec::new(),
a2ui_replies: Vec::new(),

View File

@@ -175,6 +175,7 @@ impl App {
self.remember_project(project_id);
self.selected_session = None;
self.conversation.clear();
self.context_notice = None;
self.clear_a2ui();
self.composer.clear();
self.queued_inputs.clear();
@@ -188,6 +189,7 @@ impl App {
pub(super) fn discard_session(&mut self, project_id: i32) {
if self.drafts.remove(&project_id).is_some() && self.draft_selected(project_id) {
self.conversation.clear();
self.context_notice = None;
self.clear_a2ui();
self.composer.clear();
self.queued_inputs.clear();

View File

@@ -53,16 +53,46 @@ const TRAFFIC_LIGHT_WIDTH: f32 = 78.0;
impl App {
pub(crate) fn view(&self, id: window::Id) -> Element<'_, Message> {
if self.model_manager_window == Some(id) {
let content = if self.model_manager_window == Some(id) {
self.model_manager()
} else if self.help_window == Some(id) {
self.help_view()
} else {
let content = self.main_view();
#[cfg(target_os = "macos")]
return crate::native_edit::native_edit(content, self.native_edit_commands.clone())
.into();
#[cfg(not(target_os = "macos"))]
content
}
self.main_view()
};
#[cfg(target_os = "macos")]
return crate::native_edit::native_edit(
content,
self.native_edit_commands.clone(),
self.native_edit_availability.clone(),
)
.into();
#[cfg(not(target_os = "macos"))]
content
}
fn help_view(&self) -> Element<'_, Message> {
container(
scrollable(
container(
markdown::view(
self.help_markdown.items(),
markdown::Settings::with_text_size(
15,
markdown::Style::from_palette(app_theme().palette()),
),
)
.map(Message::OpenLink),
)
.max_width(760)
.padding(32),
)
.height(Length::Fill),
)
.center_x(Length::Fill)
.height(Length::Fill)
.style(sidebar_style)
.into()
}
/// Whether a dialog covers the window. Focus moves through the whole widget

View File

@@ -55,7 +55,6 @@ impl App {
.spacing(8),
);
} else {
let markdown_style = markdown::Style::from_palette(app_theme().palette());
for (index, message) in self.conversation.iter().enumerate() {
if message.compaction {
messages = messages.push(
@@ -124,22 +123,17 @@ impl App {
}
}
if !message.content.is_empty() {
if message.user || message.tool || message.markdown.items().is_empty() {
let content = if message.reasoning.is_some() {
crate::agent::visible_content(&message.content).trim_start()
} else {
crate::agent::visible_content(&message.content)
};
body = body.push(text(content).size(14));
} else {
body = body.push(
markdown::view(
message.markdown.items(),
markdown::Settings::with_text_size(14, markdown_style),
)
.map(Message::OpenLink),
);
}
body = body.push(
text_editor(&message.transcript)
.id(iced::widget::Id::from(format!(
"transcript-{}-{index}",
message.id
)))
.on_action(move |action| Message::TranscriptAction(index, action))
.padding(0)
.size(14)
.style(selectable_text_style),
);
} else if active && message.reasoning.is_none() {
body = body.push(
text(self.activity.as_deref().unwrap_or("Loading model…")).size(14),
@@ -292,6 +286,20 @@ impl App {
.spacing(6)
.align_y(Alignment::Center),
);
if let Some(notice) = &self.context_notice {
messages = messages.push(
container(
column![
text("Context rebuild").size(11),
text(notice).size(13).color(muted_text()),
]
.spacing(5),
)
.padding(14)
.width(Length::Fill)
.style(preference_group_style),
);
}
let transcript = scrollable(messages)
.id(chat_scroll_id())
.height(Length::Fill);
@@ -337,6 +345,19 @@ impl App {
}
}
fn selectable_text_style(
theme: &Theme,
_status: iced::widget::text_editor::Status,
) -> iced::widget::text_editor::Style {
iced::widget::text_editor::Style {
background: Background::Color(Color::TRANSPARENT),
border: Border::default(),
placeholder: muted_text(),
value: theme.palette().text,
selection: theme.extended_palette().primary.weak.color,
}
}
fn generation_summary(stats: crate::app::generation::GenerationStats) -> Element<'static, Message> {
column![
row![