用户定案(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 条全绿
114 lines
4.8 KiB
Rust
114 lines
4.8 KiB
Rust
//! 占位样本数据:一段内置会话 + 出图矩阵要用的 `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)
|
||
}
|