fix(app-iced): M104 r2 —— Route::Agent ⟺ Detached 的不变式 + 复审 P3 六条
p2-1(唯一 p2):`Route::Agent` 与 `RailState::Detached` 之前没有绑定——从左侧导航第五项 (本 mission 点名的入口)进独立页时档位还是 Collapsed/Expanded,于是页头出现「Open assistant」 「Collapse/Pop out」、挂着不化的未读胶囊、Esc 也收不走页面(复审用产品 bin 复现)。 - **单一入口**:`App::sync_agent_route()`(update/agent.rs)在 `refresh_view` 开头调用, 两条对向规则:在独立页 ⇒ Detached(顺带清未读);离开独立页而档位还是 Detached ⇒ 收回右栗。 每一次路由变化都经过 refresh_view,所以导航 / ULL_START_ROUTE / detach / back 共用这一处。 - **结构性兜底**:`conversation::actions` 先按宿主分流,`Host::Page` 的动作恒为「Dock back」。 - **启动顺序**:`boot_agent()`(窗口尺寸 adopt + 拆出档直达独立页)必须在 `dispatch(RefreshAll)` 之前——那次 dispatch 会 pump→auto_select 把路由拉到实例详情,同步器随即把档位收回(实测踩到)。 - 测试:`entering_the_agent_route_detaches_the_rail`(走 `Message::Select(NavItem::Ai)` 真路径, 含未读清零 / Esc 离开 / 从页上点别的导航收回)、`the_page_navigation_entry_also_detaches`。 变异自证:把 `sync_agent_route` 改成 no-op ⇒ 3 红(含这两条)。 - 图 13/14:导航入口(折叠档带 2 条未读 / 展开档)进页后都是 Dock back、无未读胶囊、副标题单行。 P3-2 Enter/Shift+Enter 有测试了:分流抽成纯函数 `composer::binding_for`, `enter_sends_and_shift_enter_breaks_the_line` 覆盖「发送 / 换行 / 未聚焦不抢 / 其它键交回默认」; `AgentUi::edit` 里与 fallthrough 等价的冗余 Enter 分支删掉;三档测试改走 `agent.edit(...)`。 P3-4 自证式断言换字面量(320+8+852=1180 等);变异 RESIZE_GUTTER 8→0 ⇒ 6 红(含这条)。 P3-5 两条死文案真用上:8px 拖拽缝有了 tooltip(AgentResize + AgentWidthLabel「当前多宽」)。 P3-6 `on_boot` 的两块逻辑搬进 `update/agent.rs::boot_agent`:update.rs 372→383(+11,见报告逐行)。 P3-7 拖拽层补 on_press/on_exit:窗外松手不再僵住(窗内按下→清态;离窗→收在当前值)。 P3-9 顺手:只有几何/路由会变的消息才重建视图模型(每个字符、每次指针移动不再重建)。 门禁:fmt 0 / clippy(workspace,-D warnings) 0 / pages_demo 0 / 全文件 ≤400(最大 view.rs 373 之外 conversation.rs 383、update.rs 383)
This commit is contained in:
1 parent
8db1c5ab16
commit
9253684387
11 files changed
+495
-208
No files matched your search
@@ -8,8 +8,9 @@ use unlucky_i18n::{Catalog, Key, Lang};
|
||||
|
||||
use crate::theme::{Families, Ui};
|
||||
|
||||
mod drag;
|
||||
|
||||
use super::conversation::{Conversation, Host};
|
||||
use super::metrics::{layout_for, PRESETS, RESIZE_GUTTER};
|
||||
use super::state::{AgentUi, Focus, RailState, Speaker, Turn};
|
||||
use super::*;
|
||||
|
||||
@@ -19,14 +20,15 @@ const FAMILIES: Families = Families {
|
||||
mono: "Consolas",
|
||||
};
|
||||
|
||||
fn ui() -> Ui {
|
||||
pub(super) fn ui() -> Ui {
|
||||
Ui::with_platform(SchemeFlavor::Light, Platform::Windows, FAMILIES)
|
||||
}
|
||||
|
||||
/// 三档迁移:会话、草稿、未读的语义。
|
||||
/// 三档迁移:会话与草稿跟着走、未读按「打开即读」清零。
|
||||
///
|
||||
/// 抓得住:拆出去再收回,话说了一半还在(transcript + draft 不丢);未读只在收起态累积。
|
||||
/// 抓不住:切换时有没有一次「闪一下」的重排(那要出图看)。
|
||||
/// 抓得住:拆出去再收回,**transcript 与 draft** 不丢(`busy` 不在这条测试的断言里);
|
||||
/// `expand()/detach()/dock()` 都把 `unread` 清零;`collapse()` 不清(收起后新消息还要能点亮)。
|
||||
/// 抓不住:切换时有没有一次「闪一下」的重排(那要出图看),也不测 `busy` 的迁移。
|
||||
#[test]
|
||||
fn the_three_states_share_one_conversation() {
|
||||
let mut agent = AgentUi::new();
|
||||
@@ -37,14 +39,17 @@ fn the_three_states_share_one_conversation() {
|
||||
assert_eq!(agent.rail, RailState::Expanded);
|
||||
assert_eq!(agent.unread, 0, "展开即读");
|
||||
|
||||
// 走 `AgentUi::edit`(= app 层真正的落点),而不是直接操作 iced 的 `Content`:
|
||||
// 走到这里的 `Edit(Enter)` 只可能是 Shift+Enter(非 shift 的 Enter 在 composer 的
|
||||
// `key_binding` 里就变成 `Message::Submit` 了,见 composer 的 `enter_sends_and_shift_enter_breaks_the_line`)。
|
||||
agent.draft = iced::widget::text_editor::Content::with_text("kick off Survival");
|
||||
agent.draft.perform(iced::widget::text_editor::Action::Edit(
|
||||
agent.edit(iced::widget::text_editor::Action::Edit(
|
||||
iced::widget::text_editor::Edit::Enter,
|
||||
));
|
||||
assert_eq!(
|
||||
agent.draft.line_count(),
|
||||
2,
|
||||
"多行:Enter 的编辑动作能落进草稿"
|
||||
"多行:一条 Edit(Enter) 经过 `edit()` 之后草稿变成两行"
|
||||
);
|
||||
|
||||
let turns = agent.transcript.len();
|
||||
@@ -184,6 +189,7 @@ fn every_action_has_a_readable_label() {
|
||||
Key::AgentStop,
|
||||
Key::AgentStripLabel,
|
||||
Key::AgentResize,
|
||||
Key::AgentWidthLabel,
|
||||
] {
|
||||
assert!(
|
||||
!catalog.text(key).trim().is_empty(),
|
||||
@@ -305,74 +311,6 @@ fn every_state_builds_in_both_hosts() {
|
||||
}
|
||||
}
|
||||
|
||||
/// 拖拽层只在拖拽期间存在(否则它会吃掉整窗的点击)。
|
||||
#[test]
|
||||
fn the_drag_overlay_only_exists_while_dragging() {
|
||||
let mut agent = AgentUi::new();
|
||||
agent.expand();
|
||||
assert!(drag_overlay(&agent).is_none());
|
||||
|
||||
agent.drag_start();
|
||||
assert!(agent.is_dragging());
|
||||
assert!(drag_overlay(&agent).is_some());
|
||||
|
||||
agent.drag_end();
|
||||
assert!(drag_overlay(&agent).is_none());
|
||||
}
|
||||
|
||||
/// 拆出档右栗必须**不占地**(否则会话会在两个地方各画一份)。
|
||||
#[test]
|
||||
fn the_detached_state_frees_the_rail_slot() {
|
||||
let ui = ui();
|
||||
let mut agent = AgentUi::new();
|
||||
agent.detach();
|
||||
assert!(docked(&ui, &agent, 1180.0).is_none());
|
||||
assert_eq!(agent.rail_width(1180.0), 0.0);
|
||||
assert_eq!(agent.content_width(1180.0), 1180.0);
|
||||
}
|
||||
|
||||
/// 拖拽把「展开宽度」写进状态(松手后保有该宽度)。
|
||||
#[test]
|
||||
fn the_drag_writes_back_the_snapped_width() {
|
||||
let mut agent = AgentUi::new();
|
||||
agent.expand();
|
||||
agent.drag_start();
|
||||
agent.drag_to(1180.0, 1180.0 - 280.0);
|
||||
agent.drag_end();
|
||||
assert_eq!(agent.expanded(), 280.0);
|
||||
assert!(!agent.is_dragging());
|
||||
|
||||
// 没按下就收到的 DragTo(例如上一步松手后的残留)不该改宽度
|
||||
let before = agent.expanded();
|
||||
agent.drag_to(1180.0, 100.0);
|
||||
assert_eq!(agent.expanded(), before);
|
||||
|
||||
// 三档预设都能被拖到(1180 下上限 612 > 360)
|
||||
for preset in PRESETS {
|
||||
agent.drag_start();
|
||||
agent.drag_to(1180.0, 1180.0 - preset);
|
||||
agent.drag_end();
|
||||
assert_eq!(agent.expanded(), preset);
|
||||
}
|
||||
}
|
||||
|
||||
/// 面板与中栗之间的缝在布局里被真的留出来了(宽度常量与 `layout_for` 同源)。
|
||||
#[test]
|
||||
fn the_resize_gutter_is_part_of_the_budget() {
|
||||
let mut agent = AgentUi::new();
|
||||
agent.expand();
|
||||
assert_eq!(
|
||||
agent.rail_width(1180.0) + RESIZE_GUTTER,
|
||||
1180.0 - agent.content_width(1180.0)
|
||||
);
|
||||
|
||||
// 折叠与拆出档没有缝
|
||||
let collapsed = layout_for(1180.0, RailState::Collapsed, 320.0);
|
||||
assert_eq!(collapsed.gutter, 0.0);
|
||||
let detached = layout_for(1180.0, RailState::Detached, 320.0);
|
||||
assert_eq!(detached.gutter, 0.0);
|
||||
}
|
||||
|
||||
/// 会话数据结构不许在手边偷偷长字段(`Turn` 只有说话人 + 正文)。
|
||||
#[test]
|
||||
fn a_turn_is_only_a_speaker_and_a_body() {
|
||||
|
||||
Reference in new issue
Block a user