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

+34 -11
View File
@@ -12,7 +12,7 @@ use unlucky_presenter::state::AppState;
use crate::app::components::interaction::Interaction;
use crate::cli::Screen;
use crate::pages::{self, Detail, Meta, Row};
use crate::shell::{self, NavItem};
use crate::shell::{self, agent, NavItem};
use super::message::{nav_item_of, Message};
use super::state::{log_first_frame, App};
@@ -57,6 +57,13 @@ impl App {
pages::appearance::layer(&self.ui, &self.appearance, (self.width, self.height))
.map(|element| element.map(Message::Page));
// 右栗(M104):`Detached` 档返回 `None`(会话已经在独立页里);拖拽期间另有一层
// 铺满窗口的取指针层,所以两件东西分开传。
let right = agent::docked(&self.ui, &self.agent, self.width)
.map(|element| element.map(Message::Agent));
let overlay =
agent::drag_overlay(&self.agent).map(|element| element.map(Message::Agent));
shell::view_with_background(
&self.ui,
nav_item_of(self.state.route()),
@@ -73,19 +80,31 @@ impl App {
},
)
.map(Message::Page),
right,
overlay,
self.options.force_state,
)
}
// `Route::Agent`:AI 独立页(M104)。它与右栗**共用**同一个会话视图
// (`agent::page` 内部调的是 `conversation::view`),这里只是把会话摆进页面槽位。
None => {
let background =
pages::appearance::layer(&self.ui, &self.appearance, (self.width, self.height))
.map(|element| element.map(Message::Page));
let overlay =
agent::drag_overlay(&self.agent).map(|element| element.map(Message::Agent));
shell::view_with_background(
&self.ui,
NavItem::Ai,
APP_TITLE,
background,
agent::page(&self.ui, &self.agent, self.width).map(Message::Agent),
None,
overlay,
self.options.force_state,
)
}
// `Route::Agent`:AI 独立页还没有页面实现(P1),回退到 M68 的外壳卡(同一套装饰)。
None => shell::view(
&self.ui,
NavItem::Ai,
APP_TITLE,
NavItem::Ai.label(),
"The assistant page is not built yet (P1). Chat lives in the docked panel once \
the AI mission lands; this screen is the M68 skeleton fallback.",
self.options.force_state,
),
}
}
}
@@ -95,6 +114,10 @@ impl App {
/// 把 presenter 的快照转成页面视图模型(**唯一**的展示转换点)。
///
/// `forced` 是 `--force-state` 的落点(P3-1):它只影响**画出来的**状态层,不改任何状态机。
///
/// `width` 是**中栗可用宽度**(窗口 − 右栗 − 拖拽缝,由 `AgentUi::content_width` 算),不是窗口宽:
/// 页面布局系统仍按「一个窗口宽」扣左轨与页边距,所以右栗占掉的那一段必须在这里先减掉——
/// 否则助手栏一展开,页面还按 1180 排版,内容就顶到面板底下了(任务里的「收缩而不是溢出」)。
pub(super) fn model(
state: &AppState,
width: f32,