AskaEth a22baebe70 fix(app-iced): M119 r1 —— 常驻「模拟」标识 + 确认条的键盘通路
复审 r1 的两条 p2,都是「说成真的、其实是假的」与「能走进死路」这个级别的问题。

## P2-1:界面必须自己说清「这是模拟」

原判:生产默认后端是**脚本化 rig**(`ULL_AGENT_SCRIPT` 的 6 种剧情),而「后端未接 / 模型还没
接上」两句诚实文案被我改成了中性话,理由写着「由端点标签说」——**那条端点标签在壳里根本不存在**。
于是默认配置下用户敲一句话得到一段像是模型编的回答。判据:没读过代码的用户,打开右栏、敲一句话,
必须能自己看出来这不是真模型(不接受 tooltip、不接受藏在设置里)。

修法(①恢复诚实文案 + ②常驻可见标记,并用):

| 位置 | 文案 / 形态 | 谁给的 |
|---|---|---|
| 标题右侧**常驻胶囊**(`SurfaceVariant` tonal)| `Key::AgentSimulated`(Simulated / 模拟)| `AgentHost::is_simulated()` → `Session.simulated` → 视图 |
| 标题下副标题 | 「模拟助手 · 还没接真实模型」 | `Key::AgentSubtitle`(原本是「未接」被改成中性的「会话」,现在写回来且更明确)|
| 空会话正文 | 「…当前是模拟助手,没有接真实模型。」 | `Key::AgentEmptyBody` |
| 输入框占位符 | 「给模拟助手发消息…」 | `Key::AgentPlaceholder` |

**标记不是硬编码在视图里**:`AgentHost.simulated` 是事实字段(今天 transport 只有 rig ⇒ 恒 true)。
接真 `AgentTransport` 那天把它传 `false`,四个位置的「模拟」措辞会一起消失——这是为什么用字段而不是
写死一句话。

## P2-2:确认条必须有键盘通路

原判:Allow/Deny 只有鼠标路径 ⇒ 键盘用户在写操作确认处**永久卡死**(`busy` 锁发送、
`Stop` 走主后端、清不掉 presenter 的 pending confirm)。

- 焦点环多两格:`Focus::ConfirmAllow` / `Focus::ConfirmDeny`,**只在有待确认时**存在
  (鼠标用户不会 Tab 到不存在的按钮);
- 待确认出现 ⇒ `sync_confirm_focus()` 把焦点直接落到「允许」并**打开键盘巡航**(阻塞态主动亮环),
  消失 ⇒ 焦点收回输入框、巡航关闭;
- `Enter` 与 `Space` 都映射到 `Activate`(`Space` 不抢输入框:`keyboard_mode` 那道闸在
  鼠标点进输入框时是关的);
- 焦点环不是 10% 状态层——在**实心主色**按钮上那层几乎看不见(出图实测过一版就是没有)。
  改成 2dp `Primary` 描边 + 3px 留白,两个按钮恒定加这层 padding,所以焦点变化不移动布局。

测试(`crates/app-iced/src/app/update/agent/tests.rs`):
`a_blocking_confirm_can_be_decided_with_the_keyboard_alone` —— 全程只有键盘消息:
确认到达后断言 `focus == ConfirmAllow && keyboard_mode`,发一条 `KeyPressed(Enter)` 完成允许;
第二轮 `Tab`(走到 Deny)+ `KeyPressed(Space)` 完成拒绝;两轮各记一条审计、焦点都收回 Composer。
另有 `the_default_backend_reports_itself_as_simulated` 钉住 P2-1 的事实来源。

## 视图入参顺带收成结构体

`docked`/`page` 本来是 6 个形参(`ui, agent, rail, failure, catalog, width`),这一轮还要加
`simulated` ⇒ 7 个。按塔台登记的可维护性口径改成 `Session { agent, rail, failure, simulated }`:
两个宿主各 3 个形参,以后再加展示事实只改 `Session`。

## 出图(18 张,浅深各一)

`DevDocs/artifacts/m119/`:新增 `08-agent-simulated-*`(空会话 + 四处「模拟」措辞)与
`09-agent-confirm-focus-*`(确认条 + 焦点环落在「允许」);其余 7 组全部重出(文案与焦点环都变了)。

## 门禁(真实退出码)

| 命令 | 退出码 |
|---|---|
| `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(601 passed / 0 failed / 2 ignored)|
| `cargo test --workspace --exclude unlucky-app` | 0(58 套 2277 passed / 0 failed / 11 ignored)|

`golden.rs` 400 行(+1 条 `AgentSimulated` 正好顶线);全树受检文件无 >400。
2026-11-05 09:17:50 +08:00
2026-10-31 21:54:10 +08:00
2026-10-31 21:54:10 +08:00
2026-10-31 21:54:10 +08:00
2026-10-31 21:54:10 +08:00
2026-10-31 21:54:10 +08:00

Unlucky Launcher (ULL)

English · 简体中文

Official project home: https://github.com/AskaEth/Unlucky-Launcher | downloads, issues and release notes all live there: https://github.com/AskaEth/Unlucky-Launcher/releases

Unlucky Launcher (ULL) is a free, open-source Minecraft: Java Edition launcher written in Rust (Apache-2.0). It ships as one native binary per platform — no installer, no bundled runtime, no WebView. The launcher embeds an AI agent client (chat / configuration / crash diagnosis) and can be attached to other agent tooling as an MCP server.

Highlights

  • Single-file native binary: the launcher itself needs no JVM, no WebView and no sidecar runtime. The JDK a game needs is downloaded and managed per instance, not as a launcher dependency.
  • One UI stack in the build: the UI is iced 0.14 + Material 3 Expressive. The earlier Slint implementation (crates/app) is kept as source-only history: it is excluded from the cargo workspace, so the workspace never builds, tests or ships it. v0.1.0 — the only tagged release so far — still carries a Slint build (see Status).
  • CPU software-rendering fallback: --software-renderer keeps the interface rendering normally on machines with no usable GPU or with broken drivers.
  • AI agent + MCP: chat, setting edits and crash diagnosis inside the launcher; ull agent serve exposes the same core as an MCP server (stdio or Streamable HTTP) to external agent tools.
  • Mod loaders: Fabric, Quilt, NeoForge, Forge, with modpack and CurseForge / Modrinth clients in the core.
  • Java management: a Temurin → Oracle → Microsoft → Zulu fallback chain, region-aware.
  • Accounts: sign in with a Microsoft account over OAuth 2.0 device code flow → Xbox Live → XSTS → api.minecraftservices.com; account tokens are kept on this machine only.
  • Engine: multi-source download (health scoring, mirrors, chunked transfers, hash while downloading) and a YAML rule-based crash diagnosis engine.
  • License: Apache-2.0 © AskaEth (YeiJ).

Status

Two tracks. They are not the same product yet, so read the headings before downloading anything.

Track A — historical: v0.1.0, Slint UI (the only tagged release so far)

Historical implementation — no longer in the default build or the release matrix. The Slint tree (crates/app) is kept in the repository as source-only reference: it is excluded from the cargo workspace, so cargo build / cargo test / cargo clippy never touch it and no artifact is built from it. Read the source if you want to see how that UI was put together; do not expect to build it. The list below is kept because it records what the v0.1.0 assets actually contain — since v0.1.0 the pipeline builds the iced binary and ships it under the same public asset names.

  • GitHub Releases v0.1.0 (2026-11-01, flagged pre-release): single-file assets for Windows x64 (a bare .exe), Linux x64 and macOS (arm64 and x64) .tar.gz. Every release also carries SHA256SUMS.txt and the LICENSE / NOTICE / README.md texts as separate assets; on Windows those texts sit next to the .exe because a bare exe cannot hold them, while the unix archives already contain them.
  • UI: Slint + Metro. This is the shipped GUI — the instance, download, account, Java, log and settings pages all run on the core engine (the in-app AI chat panel is a scripted placeholder).
  • CLI in the same binary: unlucky-launcher instances list, unlucky-launcher launch <instance>, unlucky-launcher logs <sid> and the rest of the command set run before any UI is built — no window, no renderer probe, no font scan. Commands: instances, install, launch, accounts, sessions, logs, diag, config, java, agent, plus --help / --version.
  • A second, lighter artifact ull (cargo build --release --bin ull, zero UI dependencies): the same command set from a binary that links no graphics stack at all — not even fontconfig. Servers, CI containers and Termux get ull; desktops get unlucky-launcher and one binary covers both GUI and CLI. Both are release assets from the next tagged release (the v0.1.0 assets predate this).
  • Windows ARM64 and Linux ARM64 slots are wired into the release pipeline; the next tagged release builds them. The v0.1.0 assets predate those two slots.

Track B — in progress on dev: next-generation UI (iced + Material 3 Expressive)

This track is not released yet — you can see it only by building from source.

  • Locked direction: the look is Material 3 Expressive and the framework is iced 0.14; the UI layer is written from scratch. Neither the style nor the framework will change again.
  • Done: the token crate crates/design (no UI framework, no third-party crates — real HCT color, type scale, shape scale, spacing, elevation, motion, icon geometry, per-component metrics) and the skeleton crate crates/app-iced (opens a window, self-drawn title bar and window buttons, navigation rail, colors and type taken from tokens only, GPU→CPU backend probe with automatic fallback, and the screenshot/diff channel the visual reviews use).
  • Not done: the component library (buttons, dialogs, lists, menus, search, and the rest of the M3 set), page migration, and the i18n pass. Every page in the iced binary is a placeholder.
  • Slint is retired from the build, kept as reference: crates/app is no longer a workspace member (M118) — the source stays for historical reference only: not compiled, not tested, not shipped, and no longer buildable as a workspace member. What is left of the flip is the rename: until the iced build passes equivalence acceptance its binary is still called unlucky-launcher-iced, while the release pipeline already ships it under the public unlucky-launcher asset name.

Binaries you may run into, so you can tell them apart:

Binary Crate State
unlucky-launcher-iced crates/app-iced (iced 0.14) the only GUI binary the workspace builds; the release pipeline stages it under the public name unlucky-launcher; same subcommand dispatch
unlucky-launcher crates/app (Slint) historical: that crate is excluded from the workspace, so this binary is no longer built or shipped; the v0.1.0 assets are the last Slint builds
ull crates/cli the zero-UI build of the same command set, shipped as its own asset

unlucky-launcher and ull are not duplicates. The GUI binary needs the graphics stack when it opens a window (it dlopens X11 / Wayland / GL, and links libfontconfig at load); ull needs nothing beyond glibc, so it runs on a server, a CI container or Termux where no window can exist. Pick ull when there is no desktop, unlucky-launcher <command> when there is. Both print the same CLI text and name themselves after the binary you invoked: unlucky-launcher --help starts with unlucky-launcher ·, ull --help starts with ull ·.

Subcommand dispatch

unlucky-launcher <args> first asks "is this a CLI call?" before any UI code runs:

unlucky-launcher                       # GUI
unlucky-launcher --software-renderer   # GUI, CPU renderer (unchanged)
unlucky-launcher instances list        # CLI, no window
unlucky-launcher --json launch 1.20.1  # CLI, global options before the command
unlucky-launcher --version             # CLI, prints version + OS summary

The rule is deliberately narrow, so nothing that used to work changed meaning:

  • The first argument is a bare word (not -…) → it sits in the subcommand position, so the CLI gets it. GUI flags never appear there: --screenshot / --diff / --data-dir take their values after the flag.
  • The first argument is a flag → the line is a CLI call only if crates/cli's parser also produces a real command (--help, --version, --json instances list). --data-dir X alone parses to "no subcommand", so it still opens the GUI; every GUI-only flag (--software-renderer, --screenshot*, --renderer, --theme, --diff, --force-state, …) fails CLI parsing, so it still opens the GUI.

Unknown subcommands are an error, not a GUI launch: unlucky-launcher lst prints "未知命令 lst", hints unlucky-launcher --help, and exits 1. Silently opening a window would tell a script that its typo took effect. The subcommand table itself lives in exactly one place — crates/cli/src/cli/parse.rs — so both GUI binaries and ull accept the same set by construction.

Module map

Unlucky-Launcher (cargo workspace)
├── crates/core       # domain library: zero UI dependencies, eight modules
├── crates/agent      # AI agent: ToolSpec registry + built-in client + MCP server (one core, two shapes)
├── crates/design     # design tokens: real HCT color / type / shape / spacing / elevation / motion / icons
│                     #   / component metrics — zero UI dependencies, no third-party crates
├── crates/app        # HISTORICAL Slint UI (bin `unlucky-launcher`): excluded from the workspace,
│                     #   source-only reference — never built, tested or shipped any more
├── crates/app-iced   # the iced UI (bin `unlucky-launcher-iced`): the only GUI the workspace builds
├── crates/cli        # CLI (bin `ull`): headless, zero UI
└── packaging         # single-file artifact matrix and release scripts

The eight modules of crates/core:

Module Responsibility
setting three data-directory modes, schema-based config, atomic writes, migrations
download multi-source health scoring, mirrors, chunked downloads, hash-while-downloading
game version / instance repository, snapshots and inheritance resolution
launch 10-stage launch pipeline, processes, JSONL event stream
auth Microsoft sign-in (OAuth 2.0 device code → Xbox Live → XSTS → Minecraft Services)
addon mod loader installation, modpacks, CurseForge / Modrinth
diagnose five signal collectors, YAML rule engine, hs_err parsing
platform OS abstraction: paths, processes, file locks, capability probing, runtime OS detection

Building

Prerequisites:

  • Rust stable (rust-toolchain.toml selects the toolchain automatically; the gates also need the rustfmt and clippy components)
  • Windows: MSVC toolchain (what CI uses) or MinGW-w64 (x86_64-pc-windows-gnu)
  • Linux: libxkbcommon-dev libfontconfig1-dev libgl1-mesa-dev libegl1-mesa-dev libwayland-dev libxcb1-dev — winit + wgpu/tiny-skia ask for these at build time (the Slint build also needed femtovg)
cargo fmt --all --check                                # formatting
cargo clippy --workspace --all-targets -- -D warnings  # lints (zero-warning gate)
cargo test --workspace                                 # tests
cargo build --release                                  # binaries land in target/release/

--workspace covers the eight members in the module map. crates/app is excluded at the workspace level (M118), so none of these commands compiles the Slint tree — and there is no build command for it: that crate is kept as source-only reference and can no longer be built as a workspace member.

What the build produces:

  • unlucky-launcher-iced — the GUI (iced). It is also the CLI: the same binary answers every subcommand listed above. The name is temporary — it becomes unlucky-launcher at the flip. The release artifacts already carry the public name: the pipeline builds this bin and packaging/make_artifacts.py stages it as unlucky-launcher[.exe], so download links never change.
  • unlucky-launcher — historical: no workspace build produces this binary any more; it belonged to crates/app, which is excluded. The v0.1.0 assets are the last builds under this name.
  • ull — the CLI as a standalone, UI-free binary. Same command set, no graphics stack, smaller, and the right artifact for a machine that has no display at all.

Rendering switches:

  • unlucky-launcher-iced --software-renderer forces tiny-skia (ICED_BACKEND=tiny-skia); --screenshot <png> writes one frame and exits, which is the acceptance channel CI's headless smoke job uses
  • With no usable adapter the iced build's explicit probe falls back to the CPU path on its own
  • Historical (v0.1.0's Slint build still takes it): unlucky-launcher --software-renderer, i.e. SLINT_BACKEND=winit-software, forced the CPU renderer of that build through its own fallback chain

Dependency license check (optional locally, required in CI): cargo deny check licenses bans sources.

CI gates

Every push to main and every pull request runs fmt / clippy (warnings are errors) / the full test suite, and the Linux headless render smoke is a hard gate on the same footing: a red result blocks the merge. The macOS runtime probe is not there yet — it only runs on a manual workflow_dispatch and its result is recorded, not enforced (continue-on-error), until the slot passes a first run on a real runner.

Workflow Content
.github/workflows/ci.yml fmt + clippy + test + release build (Windows / Linux matrix); Linux runtime smoke (Xvfb: renders one real frame, then asserts the PNG is not blank); macOS runtime probe (manual dispatch only)
.github/workflows/guards.yml zero-UI guard for crates/core (manifest plus cargo tree), 400-lines-per-file guard, cargo-deny license and source guard
.github/workflows/release.yml tag v* → per-slot builds → single-file artifacts + SHA256SUMS.txt → GitHub Release

Platform baseline

Minimum baseline: Windows 10; Windows 8.1 / 8 / 7 are not supported.

  • Windows 10/11 builds use the standard toolchain (MSVC / GNU), built the same way as upstream cargo
  • macOS 12+ (Apple Silicon and Intel) and Linux x64 / arm64 are supported build slots; on Linux, X11 is the preferred path today and Wayland is the long-term direction. macOS builds are unsigned, so the first launch needs the Gatekeeper quarantine attribute cleared (xattr -d com.apple.quarantine)
  • Android Termux is experimental and not promised: CLI only, GUI unverified
  • Each platform runs its own test matrix; capability differences use feature gates with runtime probing.

License and acknowledgements

  • This project: Apache License 2.0, see LICENSE; third-party notices in NOTICE.
  • Slint (https://slint.dev), Royalty-Free 2.0: obligation for the Slint build that v0.1.0 ships — the condition attaches to each distribution of an Application, not to a version number. Upstream §2 offers two forms: (a) the official AboutSlint widget on an "About" screen or dialog reachable from the top level menu (or the splash screen when there is none); (b) the official Slint attribution badge on a public page where the binaries can be downloaded. v0.1.0's settings page does embed the AboutSlint widget (crates/app/ui/pages/settings.slint, reachable from the sidebar); whether that satisfies §2(a), and the fact that the v0.1.0 release page carries a text link rather than the §2(b) badge, are not judged here — Slint's own licence text is the authority. The v0.1.0 NOTICE, that page's text and this section are records, not a statement of discharge. Later builds link no Slint, so they add nothing to this obligation. The obligation follows a Slint-linked binary: it applies to the v0.1.0 assets (and to any later build that links Slint again), not to new iced builds — crates/app is excluded from the workspace, so the pipeline no longer produces one.
  • iced (https://github.com/iced-rs/iced), MIT: the framework of the UI, together with winit (Apache-2.0) and, through it, wgpu (MIT / Apache-2.0) / tiny-skia (BSD-3-Clause).
  • Google Material Color Utilities (Apache-2.0): the color tokens in crates/design are a Rust port of its HCT / ColorSpec2021 algorithms; details and the exact ported functions are listed in NOTICE.
  • Other major dependencies: rmcp and axum (Apache-2.0 / MIT) for the MCP server, tokio (MIT), reqwest with rustls (MIT / Apache-2.0), serde / serde_json (MIT / Apache-2.0), zip / flate2 / tar (MIT / Apache-2.0). The allow-list cargo-deny enforces is in deny.toml; GPL-family dependencies may not enter the main line.

Thanks to HMCL and PCL2 (both GPL-3.0). They have been with the vast majority of Minecraft players for years, and this project has benefited a great deal from their design choices and implementation details.

S
Description
A modern MC launcher.
Readme Apache-2.0
4.8 MiB
0 Stars 1 Watchers 0 Forks
Languages
Rust 96.8%
Slint 2.8%
Python 0.3%