Files
Unlucky-Launcher/crates/app-iced/src/pages/agent/demo.rs
T
AskaEth c3f20784a8 feat(app-iced): M104 AGENTRAIL —— 右栗三档 + 布局数学 + 独立页复用同一会话视图
用户定案(D16):右侧常驻可折叠、允许拆分成独立页、中栗宽度可调(磁铁吸附)。
本 mission 只做壳:三档状态 + 布局数学 + 一份会话视图;真数据仍归 M103,一行 presenter 都不调。

- 右栗三档:Collapsed(48) / Expanded(280~360,默认 320,锚=令牌 ai_panel_width) / Detached(Route::Agent);
  会话、草稿、未读在切档时一条不丢(拆出去再收回,话说了一半还在)
- 布局数学:中栗 = 窗口 − 右栗 − 拖拽缝(8),下限 472(= 行宽上限 424 + 两侧页边距);
  右栗一展开,页面按更窄的宽度重新排版(收缩而不是溢出);拖拽磁铁吸附到 280/320/360,超上限夹紧
- 独立页与右栗共用 conversation::view 这一份实现(输入=纯数据,输出=agent::Message)
- 控件:多行输入框(Enter 发送 / Shift+Enter 换行,走 text_editor::key_binding 的 modifiers 分流)、
  发送禁用态、流式中断、未读点 + 可读计数、⌄ 收起 / ⇥ 拆出 / ⇤ 收回;Esc 收栗、Tab 焦点环、Enter 激活
- 文案全部进 crates/i18n 的新域 catalog/agent.rs(+ golden 追加 20 行),零硬编码中文
- 取证:ULL_AGENT_RAIL / ULL_AGENT_FIXTURE / ULL_AGENT_WIDTH(截图走产品 bin,标题栏与三键可见)
- 消息不走 pages::Message(外壳级视图),走 app::Message::Agent(..) → app/update/agent.rs
- 删掉 M68 骨架回落(shell::view + shell/fallback.rs):Route::Agent 现在有真页面

门禁:fmt 0 / clippy(workspace,-D warnings) 0 / pages_demo 0 / app-iced lib 418 条全绿
2026-11-04 00:21:32 +08:00

114 lines
4.8 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.
//! 占位样本数据:一段内置会话 + 出图矩阵要用的 `ULL_AGENT_*` 取证开关。
//!
//! ## 为什么是内置样本而不是 presenter
//!
//! 本 mission 只做**壳**:真数据由 M103(`presenter::agent::AgentRail` + 四个签名)供,
//! 这里一行 presenter 调用都没有(任务里点名「不许提前调用」)。样本的职责只有两个:
//! ①让三档状态在图上都有内容;②让「空会话」有对照([`FIXTURE_ENV`])。
//!
//! 样本正文是**演示数据**,与 `pages/instances/data.rs` 的 demo 同一性质:它不是界面文案,
//! 不进 i18n 词表(界面上会随真模型一起消失)。界面自己的文案(标题/按钮/空态/提示)全部走 `Key`。
//!
//! ## 取证开关(只影响这一次进程,语义与 `ULL_APPEARANCE_*` / `ULL_START_ROUTE` 一致)
//!
//! | 变量 | 取值 | 作用 |
//! |---|---|---|
//! | [`RAIL_ENV`] | `collapsed` / `expanded` / `detached` | 启动时直接落到某一档(三态矩阵) |
//! | [`FIXTURE_ENV`] | `empty` / `streaming` / `unread` / `focused` | 空会话(零消息)/ 流式中(中断键可见)/ 展开但带未读(单独看徽标)/ 键盘焦点环(`Tab` 走过一次的样子) |
//! | [`WIDTH_ENV`] | `280` / `320` / `360` | 展开档宽度(磁铁档位的对照图) |
use super::metrics::PRESETS;
use super::state::{AgentUi, RailState, Speaker, Turn};
/// 启动档位。
pub const RAIL_ENV: &str = "ULL_AGENT_RAIL";
/// 会话样本(`empty` / `streaming`)。
pub const FIXTURE_ENV: &str = "ULL_AGENT_FIXTURE";
/// 展开档宽度(只接受三档预设值)。
pub const WIDTH_ENV: &str = "ULL_AGENT_WIDTH";
/// 内置会话样本(4 条,两类说话人 + 一条工具提示)。
pub fn transcript() -> Vec<Turn> {
vec![
Turn::new(Speaker::You, "Which instance should I start for a 1.20.4 test?"),
Turn::new(
Speaker::Assistant,
"Survival is on 1.20.4 with Fabric 0.16.5 and its files are present. Redstone Lab is the same \
version but its folder is missing a jar.",
),
Turn::new(Speaker::You, "Use Survival then. Anything I should know first?"),
Turn::new(
Speaker::Assistant,
"Java 21 is already adopted for that instance, and the last launch exited normally. \
Say the word and I will queue it.",
),
]
}
/// 按环境变量构造一份状态(没设变量 = `None`,调用方用出厂态)。
///
/// 解析失败一律**忽略**(取证开关不该让进程起不来),并在 stderr 留一行,方便对照日志读图。
pub fn from_env() -> Option<AgentUi> {
let rail = std::env::var(RAIL_ENV).ok();
let fixture = std::env::var(FIXTURE_ENV).ok();
let width = std::env::var(WIDTH_ENV).ok();
if rail.is_none() && fixture.is_none() && width.is_none() {
return None;
}
let fixture = fixture.as_deref().map(str::trim);
let mut ui = match fixture {
Some("empty") => AgentUi::empty(),
_ => AgentUi::new(),
};
if let Some(raw) = width.as_deref().map(str::trim) {
match raw.parse::<f32>() {
Ok(value) if PRESETS.contains(&value) => ui.set_expanded(value),
_ => eprintln!("[agent] {WIDTH_ENV}={raw:?} 不是预设档位 {PRESETS:?} 之一,忽略"),
}
}
if fixture == Some("streaming") {
ui.pretend_streaming();
}
// 档位走**真迁移**(`expand` / `detach` / `collapse`)而不是直接写字段:这样未读的语义
// (展开即读)在取证图里也成立,不会出现「展开着还挂着 2 条未读」这种自相矛盾的画面。
// 唯一要单独看徽标的情形用 `ULL_AGENT_FIXTURE=unread` 显式造(见模块文档的表)。
match rail.as_deref().map(str::trim) {
None => ui.expand(),
Some(name) => match RailState::from_name(name) {
Some(RailState::Collapsed) => ui.collapse(),
Some(RailState::Detached) => ui.detach(),
Some(RailState::Expanded) => ui.expand(),
None => eprintln!(
"[agent] {RAIL_ENV}={name:?} 不是三档之一({}),忽略",
RailState::ALL.map(RailState::name).join("/")
),
},
}
if fixture == Some("unread") {
ui.unread = 3;
}
if fixture == Some("focused") {
// 键盘巡航一次的样子(焦点环 = 10% 状态层)。真窗口里按一次 `Tab` 就会到这一态,
// 但截图通道收不到键盘输入,所以只能这样取证。
ui.step_focus(false);
}
eprintln!(
"[agent] rail={} width={} turns={} busy={} unread={}",
ui.rail.name(),
ui.expanded(),
ui.transcript.len(),
ui.busy,
ui.unread
);
Some(ui)
}