复审 r1 的两条 p2,都是「说成真的、其实是假的」与「能走进死路」这个级别的问题。
## P2-1:界面必须自己说清「这是模拟」
原判:生产默认后端是**脚本化 rig**(`ULL_AGENT_SCRIPT` 的 6 种剧情),而「后端未接 / 模型还没
接上」两句诚实文案被我改成了中性话,理由写着「由端点标签说」——**那条端点标签在壳里根本不存在**。
于是默认配置下用户敲一句话得到一段像是模型编的回答。判据:没读过代码的用户,打开右栏、敲一句话,
必须能自己看出来这不是真模型(不接受 tooltip、不接受藏在设置里)。
修法(①恢复诚实文案 + ②常驻可见标记,并用):
| 位置 | 文案 / 形态 | 谁给的 |
|---|---|---|
| 标题右侧**常驻胶囊**(`SurfaceVariant` tonal)| `Key::AgentSimulated`(Simulated / 模拟)| `AgentHost::is_simulated()` → `Session.simulated` → 视图 |
| 标题下副标题 | 「模拟助手 · 还没接真实模型」 | `Key::AgentSubtitle`(原本是「未接」被改成中性的「会话」,现在写回来且更明确)|
| 空会话正文 | 「…当前是模拟助手,没有接真实模型。」 | `Key::AgentEmptyBody` |
| 输入框占位符 | 「给模拟助手发消息…」 | `Key::AgentPlaceholder` |
**标记不是硬编码在视图里**:`AgentHost.simulated` 是事实字段(今天 transport 只有 rig ⇒ 恒 true)。
接真 `AgentTransport` 那天把它传 `false`,四个位置的「模拟」措辞会一起消失——这是为什么用字段而不是
写死一句话。
## P2-2:确认条必须有键盘通路
原判:Allow/Deny 只有鼠标路径 ⇒ 键盘用户在写操作确认处**永久卡死**(`busy` 锁发送、
`Stop` 走主后端、清不掉 presenter 的 pending confirm)。
- 焦点环多两格:`Focus::ConfirmAllow` / `Focus::ConfirmDeny`,**只在有待确认时**存在
(鼠标用户不会 Tab 到不存在的按钮);
- 待确认出现 ⇒ `sync_confirm_focus()` 把焦点直接落到「允许」并**打开键盘巡航**(阻塞态主动亮环),
消失 ⇒ 焦点收回输入框、巡航关闭;
- `Enter` 与 `Space` 都映射到 `Activate`(`Space` 不抢输入框:`keyboard_mode` 那道闸在
鼠标点进输入框时是关的);
- 焦点环不是 10% 状态层——在**实心主色**按钮上那层几乎看不见(出图实测过一版就是没有)。
改成 2dp `Primary` 描边 + 3px 留白,两个按钮恒定加这层 padding,所以焦点变化不移动布局。
测试(`crates/app-iced/src/app/update/agent/tests.rs`):
`a_blocking_confirm_can_be_decided_with_the_keyboard_alone` —— 全程只有键盘消息:
确认到达后断言 `focus == ConfirmAllow && keyboard_mode`,发一条 `KeyPressed(Enter)` 完成允许;
第二轮 `Tab`(走到 Deny)+ `KeyPressed(Space)` 完成拒绝;两轮各记一条审计、焦点都收回 Composer。
另有 `the_default_backend_reports_itself_as_simulated` 钉住 P2-1 的事实来源。
## 视图入参顺带收成结构体
`docked`/`page` 本来是 6 个形参(`ui, agent, rail, failure, catalog, width`),这一轮还要加
`simulated` ⇒ 7 个。按塔台登记的可维护性口径改成 `Session { agent, rail, failure, simulated }`:
两个宿主各 3 个形参,以后再加展示事实只改 `Session`。
## 出图(18 张,浅深各一)
`DevDocs/artifacts/m119/`:新增 `08-agent-simulated-*`(空会话 + 四处「模拟」措辞)与
`09-agent-confirm-focus-*`(确认条 + 焦点环落在「允许」);其余 7 组全部重出(文案与焦点环都变了)。
## 门禁(真实退出码)
| 命令 | 退出码 |
|---|---|
| `cargo fmt --all --check` | 0 |
| `cargo clippy -p unlucky-app-iced -p unlucky-presenter -p unlucky-agent -p unlucky-i18n --all-targets -- -D warnings` | 0(0 warning)|
| `cargo test -p unlucky-app-iced --lib` | 0(601 passed / 0 failed / 2 ignored)|
| `cargo test --workspace --exclude unlucky-app` | 0(58 套 2277 passed / 0 failed / 11 ignored)|
`golden.rs` 400 行(+1 条 `AgentSimulated` 正好顶线);全树受检文件无 >400。
151 lines
5.6 KiB
Rust
151 lines
5.6 KiB
Rust
//! 拖拽与「缝」的判据(从 `tests.rs` 拆出来:那边加完复审 r1 的断言后顶到 436 行,
|
||
//! 而 AGENTS.md §3-1 是 400 行硬线)。
|
||
//!
|
||
//! 拆的位置是「指针/几何」这一族:拖拽三条收尾路径、吸附宽度写回、缝的预算、缝与折叠条的标签。
|
||
|
||
use unlucky_presenter::agent::AgentRail;
|
||
|
||
use super::super::metrics::{layout_for, PRESETS, RESIZE_GUTTER};
|
||
use super::super::state::{AgentUi, RailState};
|
||
|
||
use super::super::{docked, drag_overlay};
|
||
use super::ui;
|
||
|
||
/// 拖拽层只在拖拽期间存在(否则它会吃掉整窗的点击)。
|
||
#[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();
|
||
let session = super::super::Session {
|
||
agent: &agent,
|
||
rail: &AgentRail::new(),
|
||
failure: None,
|
||
simulated: true,
|
||
};
|
||
assert!(docked(
|
||
&ui,
|
||
session,
|
||
unlucky_i18n::Catalog::of(unlucky_i18n::Lang::En),
|
||
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);
|
||
}
|
||
}
|
||
|
||
/// 面板与中栗之间的缝在布局里被真的留出来了。
|
||
///
|
||
/// **全用字面量**(复审 P3-4:早先写成 `rail + GUTTER == window − content`,两边同一组常量,
|
||
/// 把 `RESIZE_GUTTER` 改成 0 照样绿 = 自证)。现在的算式是写死的:
|
||
/// `320(面板)+ 8(缝)+ 852(中栗)= 1180`。
|
||
#[test]
|
||
fn the_resize_gutter_is_part_of_the_budget() {
|
||
let mut agent = AgentUi::new();
|
||
agent.expand();
|
||
|
||
assert_eq!(agent.rail_width(1180.0), 320.0, "1180 下展开档 = 令牌 320");
|
||
assert_eq!(agent.content_width(1180.0), 852.0, "中栗 = 1180 − 320 − 8");
|
||
assert_eq!(320.0 + 8.0 + 852.0, 1180.0, "三段之和必须回到窗口宽");
|
||
assert_eq!(RESIZE_GUTTER, 8.0, "缝宽就是 8(`Space::X2`)");
|
||
|
||
// 折叠档 48 / 拆出档 0:这两档**没有**缝(中栗 = 1180 − 48 / 1180 − 0)
|
||
let collapsed = layout_for(1180.0, RailState::Collapsed, 320.0);
|
||
assert_eq!(
|
||
(collapsed.rail, collapsed.gutter, collapsed.content),
|
||
(48.0, 0.0, 1132.0)
|
||
);
|
||
let detached = layout_for(1180.0, RailState::Detached, 320.0);
|
||
assert_eq!(
|
||
(detached.rail, detached.gutter, detached.content),
|
||
(0.0, 0.0, 1180.0)
|
||
);
|
||
}
|
||
|
||
/// 拖拽的三条收尾路径里,**能测的两条**(复审 P3-7)。
|
||
///
|
||
/// 真实鼠标在窗口外松手无法注入(这台机器连 `SetCursorPos` 都失败),所以这里只钉状态机:
|
||
/// ①松手(`DragEnd`)之后指针再动不改宽度;②拖拽中「再按一次」(叠层的 `on_press`)能清态,
|
||
/// 也就是上一条事件丢了的兜底。叠层本身挂了 `on_exit`(指针离窗 = 收在当前值),那条只能真窗口验。
|
||
#[test]
|
||
fn the_drag_recovers_when_a_release_is_missed() {
|
||
let mut agent = AgentUi::new();
|
||
agent.expand();
|
||
|
||
// ① 正常松手
|
||
agent.drag_start();
|
||
agent.drag_to(1180.0, 1180.0 - 360.0);
|
||
agent.drag_end();
|
||
assert_eq!(agent.expanded(), 360.0);
|
||
agent.drag_to(1180.0, 900.0);
|
||
assert_eq!(agent.expanded(), 360.0, "松手后指针再动不该改宽度");
|
||
|
||
// ② 拖拽中「再按一次」= 叠层的 on_press 兜底:清态,后续 DragTo 不再改宽度
|
||
agent.drag_start();
|
||
agent.drag_to(1180.0, 1180.0 - 280.0);
|
||
assert_eq!(agent.expanded(), 280.0);
|
||
agent.drag_end(); // ← 叠层 on_press → DragEnd
|
||
assert!(
|
||
!agent.is_dragging(),
|
||
"再按一次必须能清态(窗内松手丢失时的兜底)"
|
||
);
|
||
agent.drag_to(1180.0, 100.0);
|
||
assert_eq!(agent.expanded(), 280.0);
|
||
assert!(drag_overlay(&agent).is_none(), "清态后叠层必须消失");
|
||
}
|
||
|
||
/// 折叠条与拖拽缝都有可读文字(复审 P3-5:两条 key 以前在视图里一次没用)。
|
||
///
|
||
/// 抓得住:谁把 `AgentResize` / `AgentWidthLabel` 从视图里删掉(那样它们又变回空头支票)。
|
||
/// 抓不住:tooltip 真弹出来长什么样(那要 hover 出图)。
|
||
#[test]
|
||
fn the_gutter_and_strip_are_labelled() {
|
||
let hosts = include_str!("../mod.rs");
|
||
let strip = include_str!("../strip.rs");
|
||
for key in ["Key::AgentResize", "Key::AgentWidthLabel"] {
|
||
assert!(hosts.contains(key), "视图里没用上 {key}");
|
||
}
|
||
// M119:折叠条整段搬去了 `strip.rs`(`mod.rs` 顶到 400 行)
|
||
assert!(strip.contains("Key::AgentStripLabel"), "折叠条上没画标签");
|
||
}
|