Files
Unlucky-Launcher/crates/app-iced/src/presenter/mod.rs
T
AskaEth d4fd675935 feat(app-iced): M119 把 presenter 的 agent 四契约与账户窄入口接进 iced 壳
M103 把 AI 会话的投影(`AgentRail` + `submit/mark_read/confirm`)做完了,M114 把账户三轨做完,
但 iced 壳一直没接:M103 复审确认 `Presenter::` 零非测试消费者,M104 的右栏会话数据来自内置样本。
这一个提交就是那一步——**右栏与账户页吃 presenter 的真实状态**。

## 接线层(新增 `crates/app-iced/src/presenter/`)

| 文件 | 是什么 |
|---|---|
| `agent.rs` | `AgentHost`:presenter 句柄(四契约)+ 事件队列 + **具名**失败(`AgentFailure`:`unreachable` / `not_implemented`)|
| `transport.rs` | `AgentTransport` 的 iced 侧实现(脚本化 rig + 五种剧情 + 两种失败注入)|
| `rig.rs`(仅测试) | 账户轨的 `Backend`:`AppCommand` → host 形状的 `CoreEvent` |
| `mod.rs` | 逻辑路径说明 + **线程/通道边界** |

物理目录 `src/presenter/`,逻辑名字 `crate::app::state::presenter`——由 `app/state.rs` 用
`#[path]` 挂载(与 `shell::agent` 挂 `pages/agent/` 同一个先例:scope 不含 `lib.rs`)。

## 事件回流

`transport` 只**入队**、UI 只**出队**:`advance`→`pump` 里同拍抽干,抽出来的这批经
**既有 `Message::Agent(..)` 通道**(新变体 `Events(Vec<AppEvent>)`)回到 `on_agent`,再由
`Presenter::apply` 落地。同一条漏斗,所以用户点击与后端事件不会各改一次状态。

## 会话内容只留一份

`AgentUi` 里的 `transcript/busy/unread` **删掉**,右栏与独立页直接读 `agent_rail()`
(`pages/agent::spec` 零拷贝借)。M104 的样本会话(`demo::transcript()`)一并删除:出厂就是
空会话,取证夹具要内容就**跑一轮真提交**(`ULL_AGENT_FIXTURE=turn`),消息只能由 presenter 产生。

## 视图

- 气泡按 `ChatMessage.role` 画(User/Assistant/Tool/System),没有任何壳自己的会话类型;
- 新增两条横幅(`conversation/banners.rs`):**失败态**(catalog 标题 + 已脱敏正文)与
  **写操作确认**(摘要 + Allow/Deny → `agent_confirm`);
- 拖动缝与折叠条按 400 行硬线拆到 `mod.rs` 之外(`strip.rs`、`conversation/{transcript,banners}.rs`)。

## 未读口径

presenter 判「栏可见吗」用的是它自己的 router + `prefs.ai_collapsed`,而这两件的真源在 `App`。
`refresh_view` 用 presenter 的两个**窄入口**(`navigate` / `prefs_mut().ai_collapsed`)单点写回,
带幂等闸(相同就不重复写,避免 `navigate(Route::Agent)` 顺手 mark_read 吃掉用户攒的未读)。

## 账户页

账户列表与登录事件仍走 `AppCommand`/`CoreEvent`(M114 的形状没动),本轮把**登录指令与
列表消费**纳入同一套树测试:添加离线档案 → 列表出现 → 切换 → 删除;设备码「进行中可见 + 可取消」;
失败态(名字不合法 / 凭据失效)都给具名错误。

## 树测试(9 条,`cargo test -p unlucky-app-iced --lib` 全绿:599 passed / 0 failed)

- `app/update/agent/tests.rs`:提交 → 事件回流 → 右栏出现该轮 + `busy` 翻转 + 标记已读归零;
  确认条可裁决且记审计;三种失败各给具名横幅;草稿逐字同步。
- `app/update/account/tests.rs`:增/切/删端到端;设备码进行中与取消;两种失败态;
  M114 的三条回归闸(原内联测试搬出来)。
- `pages/agent/tests.rs`:视图契约(`spec` 原样借 rail)、a11y 标签、焦点环、两种宿主全态构图。

顺带:`app/tests.rs` 里三条 `agent.unread` 断言跟着 rail 走(出厂 0 未读),`view/account_page.rs`
的 `docked` 调用点补上 rail 与失败横幅。
2026-11-05 08:02:58 +08:00

42 lines
2.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! iced 侧的 presenter 接线层(M119)。
//!
//! ## 为什么单独一个目录
//!
//! presenter 已经把「AI 会话怎么攒、未读怎么算、YOLO 怎么裁决」做完了(M103),壳层只该
//! **读视图模型 + 把点击翻成四个方法**。这份「只做翻译」的胶水需要地方放,而且要能和 presenter
//! 自己的契约分开审:所以是独立目录,不塞进 `app/` 的 update 分支里。
//!
//! | 文件 | 职责 | 不负责 |
//! |---|---|---|
//! | [`agent`] | [`AgentHost`]:presenter 句柄(四契约)+ 事件队列 + 具名错误 | 画、算脱敏 |
//! | [`transport`] | [`AgentTransport`](unlucky_presenter::agent::AgentTransport) 的 iced 侧实现(脚本化 rig + 三种失败注入) | 真实模型调用 |
//! | `rig`(仅测试) | 账户轨的 `Backend` 实现:`AppCommand` → host 形状的 `CoreEvent` | 真 AccountStore 落盘 |
//!
//! ## 逻辑路径与物理路径
//!
//! 物理目录是 `crates/app-iced/src/presenter/`,逻辑名字是 `crate::app::state::presenter`——由
//! `app/state.rs` 用 `#[path = "../presenter/mod.rs"]` 挂载。理由与 `shell::agent`
//! (`crates/app-iced/src/pages/agent/` → `crate::shell::agent`)完全相同:本 mission 的 scope
//! 不含 `lib.rs` 与 `app/mod.rs`,用 `#[path]` 挂载既不碰共用声明文件,也不产生第二份实现。
//!
//! ## 线程/通道边界(本轮只验同步形状,异步未验)
//!
//! ```text
//! on_agent(Submit) ──► AgentHost::submit ──► Presenter::agent_submit ──► Transport::submit
//! │ 事件进队列
//! 右栏(读 rag.agent_rail())◄── AgentHost::apply ◄── Message::Agent(Events) ◄── App::drain_agent
//! ```
//!
//! 队列是 `Arc<Mutex<VecDeque<..>>>`:**transport 侧只入队、UI 侧只出队**,中间不共享状态。
//! 真实异步后端落地时换掉的是「谁往队列里 push」,这条形状不动。本轮 transport 是同步实现
//! (`submit` 当场把整批事件入队),所以「跨线程时序 / 乱序 / 并发提交」**没有验过**。
pub(crate) mod agent;
// 账户轨的 UI 侧 rig(`AppCommand` → host 形状的 `CoreEvent`)。只在测试里存在:截图通道的
// 账户矩阵仍走 presenter 自带的 `MockBackend`(`ULL_ACCOUNT_STATE`)。
#[cfg(test)]
pub(crate) mod rig;
pub(crate) mod transport;
pub(crate) use agent::{AgentFailure, AgentHost};