复审 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)。
67 lines
5.0 KiB
Rust
67 lines
5.0 KiB
Rust
//! AI 助手栏 / AI 独立页的文案(M104 `AGENTRAIL`)。
|
||
//!
|
||
//! 这两处**共用同一套会话视图**(`crates/app-iced/src/pages/agent/`),所以文案也只在这里出现一次:
|
||
//! 栏上的折叠/拆出、页上的收回、两处都要的发送/中断/空会话引导。
|
||
//!
|
||
//! 域文件的形状见 [`crate::catalog`] 的「域装配」一节:本宏把条目**追加**到 `$acc` 之后,
|
||
//! 再把「还没装配的域」原样交给 `$next`。条目顺序 = `Key` 的声明顺序(金表 `catalog::golden` 守门)。
|
||
//!
|
||
//! **新增 key 时按顺序追加在最后**:金表是「拆分前那份表的逐字快照」,重排既有行会让它红。
|
||
|
||
macro_rules! agent_entries {
|
||
($next:ident, $($rest:ident,)* { $($acc:tt)* }) => {
|
||
$next! { $($rest,)* {
|
||
$($acc)*
|
||
// ── AI 助手(右栗 + 独立页共用)
|
||
AgentTitle => ("Assistant", "助手"),
|
||
// M119 r1 复审 P2-1:这条一度被改成中性的「会话」,理由是「跑什么后端由端点标签说」——
|
||
// 但端点标签在壳里根本不存在,于是默认配置下用户看到的是一个像是接上了模型的助手。
|
||
// 现在把诚实的话写回来,而且它是**常驻**的(标题下就一行,不点也能看到)。
|
||
AgentSubtitle => ("Simulated assistant · no real model is connected", "模拟助手 · 还没接真实模型"),
|
||
AgentCollapse => ("Collapse", "收起"),
|
||
AgentDetach => ("Pop out", "拆出"),
|
||
AgentDock => ("Dock back", "收回"),
|
||
AgentOpen => ("Open assistant", "打开助手"),
|
||
AgentStripLabel => ("AI", "助手"),
|
||
AgentSend => ("Send", "发送"),
|
||
AgentStop => ("Stop", "中断"),
|
||
// 输入框上再说一次:用户真要动手的位置就在这儿
|
||
AgentPlaceholder => ("Message the simulated assistant…", "给模拟助手发消息…"),
|
||
AgentEmptyTitle => ("Nothing in this session yet", "这个会话还没有内容"),
|
||
// 空会话是最容易被误读的一屏(没有人说过话,很容易以为是「连上了但还没问」)
|
||
AgentEmptyBody => ("Ask about instances, downloads or logs. This assistant is simulated: no real model is connected.", "可以问实例、下载或日志。当前是模拟助手,没有接真实模型。"),
|
||
AgentNotePending => ("Placeholder shell: your message is kept here, nothing was sent to a model.", "占位外壳:消息留在这里,没有发给任何模型。"),
|
||
AgentNoteStopped => ("Stopped by you.", "你中断了这次回复。"),
|
||
AgentUnread => ("{count} unread", "{count} 条未读"),
|
||
AgentResize => ("Drag to resize the assistant rail", "拖动调整助手栏宽度"),
|
||
AgentWidthLabel => ("Width {width}", "宽度 {width}"),
|
||
AgentRoleYou => ("You", "你"),
|
||
AgentRoleAssistant => ("Assistant", "助手"),
|
||
AgentRoleNotice => ("Notice", "提示"),
|
||
// ── M119:失败态与写操作确认(接线后才有;`AgentRail::error` / `rail.confirm`)
|
||
//
|
||
// 为什么必须成 key:任务要求「presenter 出错时 UI 显示**具名**错误」,
|
||
// 而 M106 立的规矩是界面文案一律走 catalog——没有键就只能硬编码中英。
|
||
AgentErrorTitle => ("Assistant error", "助手出错"),
|
||
AgentErrorUnavailable => ("Couldn't reach the assistant backend. Check the endpoint, then try again.", "连不上助手后端:检查端点后重试。"),
|
||
AgentErrorNotImplemented => ("This assistant request isn't wired up yet.", "这个助手请求还没接线。"),
|
||
AgentConfirmRequest => ("The assistant wants to change something: {summary}", "助手想改一项设置:{summary}"),
|
||
AgentConfirmAllow => ("Allow", "允许"),
|
||
AgentConfirmDeny => ("Deny", "拒绝"),
|
||
// 头部常驻小标记(P2-1):与副标题/占位符互为冗余,三个位置都能看到「这是模拟」
|
||
AgentSimulated => ("Simulated", "模拟"),
|
||
// ── M119 r2:三处文案的**真实态**配对(模拟态用上面那三条已有的 key)
|
||
//
|
||
// 复审 r2 的 P2-1:上一轮只有标题胶囊由 `simulated` 驱动,副标题/占位符/空态正文是
|
||
// 无条件取词,而那三条译文里写着「模拟 / 没接真实模型」——接上真 transport 之后
|
||
// 这三句会**反方向**地留着。现在配成对:`simulated == true` 走模拟态,
|
||
// `false` 走这三条中性话(`pages/agent/tests/disclaimer.rs` 钉住四处一起变)。
|
||
AgentSubtitleReal => ("Conversation", "会话"),
|
||
AgentPlaceholderReal => ("Message the assistant…", "给助手发消息…"),
|
||
AgentEmptyBodyReal => ("Ask about instances, downloads or logs.", "可以问实例、下载或日志。"),
|
||
} }
|
||
};
|
||
}
|
||
|
||
pub(crate) use agent_entries;
|