fix(app-iced): M119 r2 —— 四处「模拟」文案全由 simulated 驱动;确认条不再替用户 arm 键盘
复审 r2 的两条 p2 都是「上一轮各差一步」:字段/焦点都真的做了,但**另有三处没接上**、
**键盘闸是代码自己打开的**。
## P2-1:四处文案只认一个开关
r1 只有标题胶囊由 `simulated` 驱动;副标题 / 输入框占位符 / 空态正文是**无条件取词**,
而它们的译文里写着「模拟 / 没接真实模型」——接上真 transport 之后这三句会**反向**留着
(`simulated == false` 时界面还在声明「没接真实模型」)。而且没有一条测试钉得住那个声明。
现在:
- 新增 `pages/agent::Disclaimer { simulated, subtitle, placeholder, empty_body }`,
`Disclaimer::of(catalog, simulated)` 一次算好;`Conversation.simulated: bool` 换成
`Conversation.note: Disclaimer`,header / composer / transcript 三处各自从它取词——
**视图里没有第二种取词路径**;
- i18n 配成对:模拟态用既有的 `AgentSubtitle` / `AgentPlaceholder` / `AgentEmptyBody`,
真实态新增 `AgentSubtitleReal` / `AgentPlaceholderReal` / `AgentEmptyBodyReal`(中性话)。
(没有把三句改成中性话:那句告知本身有价值,接真模型后它才该消失。)
- 证据:`crates/app-iced/src/pages/agent/tests/disclaimer.rs`
—— `all_four_disclaimers_appear_when_the_backend_is_simulated`(四处都提模拟)、
`no_disclaimer_survives_when_a_real_backend_is_wired`(四处都不提,且不许用空串糊)、
`the_two_states_use_different_keys`(两态不能共用同一个 key)。
**golden.rs 的 400 行边界**:+3 条会破线。按「按功能拆、不压注释」处理——把这张表的
**说明文档**(35 行)原样搬到 `catalog.rs` 里 `mod golden;` 的文档上(一个字的说明都没删),
`golden.rs` 只留表数据 + 3 行指针。现在 `golden.rs` 374 行、`catalog.rs` 328 行。
## P2-2:键盘闸不许由代码替用户打开
r1 在 `sync_confirm_focus`(`on_agent` 的无条件尾巴,`Events` 又由心跳驱动)里顺手
`keyboard_mode = true`——于是**只要冒出待确认,全局键盘闸就开了**;用户发完那句话接着打下一句,
第一个空格就成了「批准写操作」。复审用临时用例复现过(`rail.confirm` 变 `None`)。
现在:
- `sync_confirm_focus` **只挪焦点**到「允许」,不再碰 `keyboard_mode`(字段文档里写清了为什么);
- 键盘用户的表态是 `Tab`:`AgentUi::step_focus` 加了一条 **arming 规则**——待确认、焦点已在
确认条上、但键盘闸还关着时,这一次 `Tab` **只点亮焦点环、不移动焦点**(否则第一次 Tab 会把
「允许」挪到「拒绝」,用户按 Enter 就是反着裁决);之后再 Tab 才逐格走;
- 反向回归:`a_stray_space_or_enter_never_approves_a_pending_write`(打字 → 空格 / 回车都不得裁决,
审计也不许留);`a_blocking_confirm_can_be_decided_with_the_keyboard_alone` 改成
「确认到达时未 armed → Tab 点亮 → Enter 允许;第二轮 Tab·Tab 到拒绝 → Space」。
## 出图
`DevDocs/artifacts/m119/` 18 张全部重出(新增取证夹具 `ULL_AGENT_FIXTURE=armed`:
跑一轮真提交后再按一次 Tab,专供「焦点环可见」那张图):
- `08-agent-simulated-{light,dark}`:`simulated = true` 下四处「模拟」同时可见;
- `09-agent-confirm-focus-{light,dark}`:**按过 Tab 之后**的确认条(焦点环在「允许」上);
- `05-agent-confirm-{light,dark}`:确认条刚出现、**还没 arm**(无焦点环)——与 09 成对,
正好把 r2 的行为差异拍下来。
## 门禁(真实退出码,最终树)
| 命令 | 退出码 |
|---|---|
| `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(605 passed / 0 failed / 2 ignored)|
| `cargo test --workspace --exclude unlucky-app` | 0(58 套 2281 passed / 0 failed / 11 ignored)|
全树受检文件无 >400(`golden.rs` 374、`catalog.rs` 328、`pages/agent/mod.rs` 371)。
This commit is contained in:
1 parent
a22baebe70
commit
0e48ea15b8
14 files changed
+332
-81
No files matched your search
@@ -187,12 +187,16 @@ impl App {
|
||||
})
|
||||
}
|
||||
|
||||
/// 待确认请求出现 ⇒ 焦点直接落在「允许」并打开键盘巡航;消失 ⇒ 收回输入框。
|
||||
/// 待确认请求出现 ⇒ 焦点直接落在「允许」;消失 ⇒ 收回输入框。
|
||||
///
|
||||
/// 为什么要**打开** `keyboard_mode`(而不是只挪焦点):待确认时 `busy` 锁着发送键,
|
||||
/// `Stop` 又走主后端、清不掉 presenter 的 pending——键盘用户若够不着这两个按钮,这一轮对话
|
||||
/// 就再也推不动(复审 P2-2 的原话)。所以这是一个**阻塞态**:焦点环主动亮出来,
|
||||
/// 按 `Enter` 或 `Space` 就能裁决。
|
||||
/// **只挪焦点,不替用户打开键盘巡航**(M119 r2 / 复审 P2-2):r1 在这里顺手把
|
||||
/// `keyboard_mode` 置真,而 `sync_confirm_focus` 是 `on_agent` 的无条件尾巴、`Events` 又由心跳
|
||||
/// 驱动——于是只要冒出待确认,全局键盘闸就被打开;用户发完那句话接着打下一句,第一个空格
|
||||
/// 就成了「批准写操作」。现在那道闸只由真实的键盘表态打开(`Tab`,见 `AgentUi::step_focus`;
|
||||
/// 或者 `on_agent_key` 把按键送进来时用户本来就在键盘巡航)。
|
||||
///
|
||||
/// 键盘用户怎么走到确认条:待确认时焦点已经在这两个按钮上,**按一次 `Tab` 点亮焦点环即是
|
||||
/// 表态**(`step_focus` 里那条 arming 规则:这一次不移动焦点),之后 `Enter`/`Space` 就能裁决。
|
||||
///
|
||||
/// 唯一写入口:`on_agent` 末尾(任何会改动 rail 的分支之后都要经过它)。
|
||||
fn sync_confirm_focus(&mut self) {
|
||||
@@ -200,8 +204,7 @@ impl App {
|
||||
let on_confirm = matches!(self.agent.focus, Focus::ConfirmAllow | Focus::ConfirmDeny);
|
||||
if pending && !on_confirm {
|
||||
self.agent.focus = Focus::ConfirmAllow;
|
||||
self.agent.keyboard_mode = true;
|
||||
eprintln!("[agent] 写操作待确认 → 焦点落到 Allow(Enter/Space 裁决)");
|
||||
eprintln!("[agent] 写操作待确认 → 焦点落到 Allow(Tab 点亮焦点环后 Enter/Space 裁决)");
|
||||
} else if !pending && on_confirm {
|
||||
self.agent.focus = Focus::Composer;
|
||||
self.agent.keyboard_mode = false;
|
||||
@@ -258,6 +261,15 @@ impl App {
|
||||
let _ = self.agent_host.submit(demo::DEMO_QUESTION.to_string());
|
||||
self.drain_agent();
|
||||
}
|
||||
Fixture::Armed => {
|
||||
// 先跑一轮(写操作脚本会挂一个待确认),再按一次 `Tab`:
|
||||
// 待确认时这一下只 arm、不移动焦点(见 `AgentUi::step_focus` 的 arming 规则)
|
||||
let _ = self.agent_host.submit(demo::DEMO_QUESTION.to_string());
|
||||
self.drain_agent();
|
||||
let rail = self.agent_host.rail();
|
||||
let (busy, pending) = (rail.busy, rail.confirm.is_some());
|
||||
self.agent.step_focus(false, busy, pending);
|
||||
}
|
||||
Fixture::Focused => {
|
||||
let rail = self.agent_host.rail();
|
||||
let (busy, pending) = (rail.busy, rail.confirm.is_some());
|
||||
|
||||
@@ -275,15 +275,27 @@ fn a_blocking_confirm_can_be_decided_with_the_keyboard_alone() {
|
||||
app.agent_host.rail().confirm.is_some(),
|
||||
"先要有一个待确认请求"
|
||||
);
|
||||
// ① 焦点主动落在「允许」,并且键盘巡航已经打开(否则 Enter 那道闸过不去)
|
||||
// ① 焦点主动落在「允许」(阻塞态),但**键盘闸不替用户打开**(复审 r2 / P2-2)
|
||||
assert_eq!(
|
||||
app.agent.focus,
|
||||
Focus::ConfirmAllow,
|
||||
"焦点要落在可操作元素上"
|
||||
);
|
||||
assert!(app.agent.keyboard_mode, "阻塞态要主动亮出焦点环");
|
||||
assert!(
|
||||
!app.agent.keyboard_mode,
|
||||
"塔台 r2:不许自动 arm——自动 arm 会让用户随手敲的空格变成「批准」"
|
||||
);
|
||||
|
||||
// ② Enter = 允许
|
||||
// ② 第一次 Tab = 「我要用键盘」的表态:只点亮焦点环,**不移动**焦点
|
||||
let _ = app.update(Message::Agent(AgentMessage::Tab { shift: false }));
|
||||
assert!(app.agent.keyboard_mode, "Tab 之后才算 armed");
|
||||
assert_eq!(
|
||||
app.agent.focus,
|
||||
Focus::ConfirmAllow,
|
||||
"arming 的那一次不移动焦点(否则 Enter 会反着裁决)"
|
||||
);
|
||||
|
||||
// ③ Enter = 允许
|
||||
let _ = app.update(Message::Agent(AgentMessage::Key(press(Named::Enter))));
|
||||
assert!(
|
||||
app.agent_host.rail().confirm.is_none(),
|
||||
@@ -293,12 +305,48 @@ fn a_blocking_confirm_can_be_decided_with_the_keyboard_alone() {
|
||||
assert_eq!(app.agent.focus, Focus::Composer, "裁决完焦点收回输入框");
|
||||
assert!(!app.agent.keyboard_mode, "阻塞态结束就不再亮焦点环");
|
||||
|
||||
// ③ 第二轮走「Tab 到拒绝 + Space 触发」:键盘用户不必背 Enter 是哪一个
|
||||
// ④ 第二轮走「Tab 点环 → Tab 走到拒绝 → Space 触发」:键盘用户不必背 Enter 是哪一个
|
||||
submit(&mut app, "那把并发改回来");
|
||||
assert_eq!(app.agent.focus, Focus::ConfirmAllow);
|
||||
let _ = app.update(Message::Agent(AgentMessage::Tab { shift: false }));
|
||||
let _ = app.update(Message::Agent(AgentMessage::Tab { shift: false }));
|
||||
assert_eq!(app.agent.focus, Focus::ConfirmDeny, "Tab 要能走到「拒绝」");
|
||||
let _ = app.update(Message::Agent(AgentMessage::Key(press(Named::Space))));
|
||||
assert!(app.agent_host.rail().confirm.is_none(), "Space 也要能裁决");
|
||||
assert_eq!(app.agent_host.audit_len(), 2);
|
||||
}
|
||||
|
||||
/// **复审 r2 / P2-2 的反向回归**:没 armed 的时候,空格与回车都不许裁决写操作。
|
||||
///
|
||||
/// 复审用临时用例复现的路径:WriteOp 提交后(r1 无条件 arm),用户接着打下一句,第一个空格就成了
|
||||
/// 「允许」。真实场景里编辑器仍持 iced 焦点,所以这条不是理论问题。
|
||||
///
|
||||
/// 抓得住:谁把 `sync_confirm_focus` 里的 `keyboard_mode = true` 加回来;谁把 `Space`/`Enter` 的
|
||||
/// `Activate` 绕到 `keyboard_mode` 闸之外。
|
||||
/// 抓不住:iced 组件树里空格到底有没有进输入框(那要真窗口,出图另算)——这里只断言「没裁决」。
|
||||
#[test]
|
||||
fn a_stray_space_or_enter_never_approves_a_pending_write() {
|
||||
let mut app = app_with(Script::WriteOp);
|
||||
submit(&mut app, "把下载并发改小");
|
||||
assert!(app.agent_host.rail().confirm.is_some());
|
||||
assert!(!app.agent.keyboard_mode, "待确认不该自动 arm");
|
||||
|
||||
// 用户接着打下一句:先敲一个字符(编辑器自己消化),再敲空格
|
||||
let _ = app.update(Message::Agent(AgentMessage::Edit(
|
||||
text_editor::Action::Edit(text_editor::Edit::Insert('a')),
|
||||
)));
|
||||
let _ = app.update(Message::Agent(AgentMessage::Key(press(Named::Space))));
|
||||
assert!(
|
||||
app.agent_host.rail().confirm.is_some(),
|
||||
"未 armed 时空格不许批准写操作"
|
||||
);
|
||||
assert_eq!(app.agent_host.audit_len(), 0, "也不许留下审计");
|
||||
|
||||
// 回车同理:未 armed 时它归编辑器(发送键),不归确认条
|
||||
let _ = app.update(Message::Agent(AgentMessage::Key(press(Named::Enter))));
|
||||
assert!(
|
||||
app.agent_host.rail().confirm.is_some(),
|
||||
"未 armed 时回车不许批准写操作"
|
||||
);
|
||||
assert_eq!(app.agent_host.audit_len(), 0);
|
||||
}
|
||||
Reference in new issue
Block a user