feat(app-iced): M104 AGENTRAIL —— 右栗三档 + 布局数学 + 独立页复用同一会话视图

用户定案(D16):右侧常驻可折叠、允许拆分成独立页、中栗宽度可调(磁铁吸附)。
本 mission 只做壳:三档状态 + 布局数学 + 一份会话视图;真数据仍归 M103,一行 presenter 都不调。

- 右栗三档:Collapsed(48) / Expanded(280~360,默认 320,锚=令牌 ai_panel_width) / Detached(Route::Agent);
  会话、草稿、未读在切档时一条不丢(拆出去再收回,话说了一半还在)
- 布局数学:中栗 = 窗口 − 右栗 − 拖拽缝(8),下限 472(= 行宽上限 424 + 两侧页边距);
  右栗一展开,页面按更窄的宽度重新排版(收缩而不是溢出);拖拽磁铁吸附到 280/320/360,超上限夹紧
- 独立页与右栗共用 conversation::view 这一份实现(输入=纯数据,输出=agent::Message)
- 控件:多行输入框(Enter 发送 / Shift+Enter 换行,走 text_editor::key_binding 的 modifiers 分流)、
  发送禁用态、流式中断、未读点 + 可读计数、⌄ 收起 / ⇥ 拆出 / ⇤ 收回;Esc 收栗、Tab 焦点环、Enter 激活
- 文案全部进 crates/i18n 的新域 catalog/agent.rs(+ golden 追加 20 行),零硬编码中文
- 取证:ULL_AGENT_RAIL / ULL_AGENT_FIXTURE / ULL_AGENT_WIDTH(截图走产品 bin,标题栏与三键可见)
- 消息不走 pages::Message(外壳级视图),走 app::Message::Agent(..) → app/update/agent.rs
- 删掉 M68 骨架回落(shell::view + shell/fallback.rs):Route::Agent 现在有真页面

门禁:fmt 0 / clippy(workspace,-D warnings) 0 / pages_demo 0 / app-iced lib 418 条全绿
This commit is contained in:
AskaEth committed 2026-11-04 00:21:32 +08:00
1 parent 875ac159ee
commit c3f20784a8
20 files changed
+2463 -153

No files matched your search

+43 -2
View File
@@ -203,20 +203,61 @@ fn idle_ticks_do_not_rebuild_the_view_model() {
assert!(app.refreshes > before, "导航之后必须重建一次");
}
/// 视图模型跟着窗口宽度走(交互模式下 600 / 840 两条断点靠 resize 事件生效)。
///
/// M104 起 `view.window_width` 装的是**中栗可用宽度**(窗口 − 右栗 − 拖拽缝),所以这里按三档
/// 各钉一个**字面量**:48 的折叠条与 320 的面板各自的减法都得对。
/// (断言里不调 `agent.content_width(..)`——那是同义反复,实现错一处它照样绿。)
#[test]
fn resizing_the_window_rebuilds_the_view_model() {
let mut app = headless(Options::default(), SchemeFlavor::Light);
let _ = app.update(Message::Boot);
// 出厂是 Collapsed(48) 档:中栗 = 窗口 − 48
let _ = app.update(Message::Window(iced::window::Event::Resized(
iced::Size::new(1180.0, 760.0),
)));
assert!((app.view.window_width - 1180.0).abs() < f32::EPSILON);
assert!(
(app.view.window_width - 1132.0).abs() < f32::EPSILON,
"1180 − 48(折叠条)= 1132,实际 {}",
app.view.window_width
);
let _ = app.update(Message::Window(iced::window::Event::Resized(
iced::Size::new(700.0, 600.0),
)));
assert!((app.view.window_width - 700.0).abs() < f32::EPSILON);
assert!(
(app.view.window_width - 652.0).abs() < f32::EPSILON,
"700 − 48(折叠条)= 652,实际 {}",
app.view.window_width
);
// Expanded(320) 档:中栗 = 窗口 − 320(面板)− 8(拖拽缝)= 852
app.agent.expand();
app.view_dirty = true;
app.refresh_view();
assert!(
(app.view.window_width - 652.0).abs() < f32::EPSILON,
"700 上展开档不成立(上限 700 − 88 − 472 − 8 = 132 < 280)⇒ 实际按折叠条 48 算: 700 − 48 = 652,实际 {}",
app.view.window_width
);
let _ = app.update(Message::Window(iced::window::Event::Resized(
iced::Size::new(1180.0, 760.0),
)));
assert!(
(app.view.window_width - 852.0).abs() < f32::EPSILON,
"1180 − 320 − 8 = 852,实际 {}",
app.view.window_width
);
// Detached 档:右栗不占宽,中栗 = 窗口 = 1180
app.agent.detach();
app.view_dirty = true;
app.refresh_view();
assert!(
(app.view.window_width - 1180.0).abs() < f32::EPSILON,
"拆出后右栗不占宽:1180 − 0 = 1180,实际 {}",
app.view.window_width
);
// 页面照旧能构图(窄窗退化到单列,不 panic)
let _ = app.view();
}