Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 15 additions & 8 deletions apps/desktop/loopx-control-plane/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,21 @@ The bundled runtime owns the CLI, HTTP APIs and workspace assets. A runtime-only
CLI update cannot patch native startup or updater bugs; those require an App
update. The App update workflow packages both layers from one Git revision.

On macOS, opening the App now automatically prepares its bundled runtime when
the default CLI is missing or has a different source revision. This changes
the previous startup behavior, which stopped at a manual repair gate. It can
replace a separately updated default CLI with the App's matching snapshot;
use an App update to move the paired installation forward. Automatic startup
does not download another App or choose another update channel. Explicit
`LOOPX_BIN` overrides are retained and are never replaced automatically: a
mismatched override must be corrected by its owner.
On macOS, opening the App prepares its bundled runtime automatically only when
no default CLI runtime is installed: with nothing to replace, a fresh machine
still bootstraps in one launch. When a *different* runtime is already selected,
the first screen asks the operator instead of replacing it, because that
default CLI may be the newer layer. The two choices are **Update App and
runtime** (check this App's channel, then install the signed App and its
matching snapshot together) and **Use this App's runtime** (install the
snapshot this App carries, which aligns the CLI to the App's revision and can
move it backwards). Services stay stopped until one of them is chosen, so an
App that lags the CLI can no longer silently downgrade the CLI on open. Neither
choice downloads another App on its own or selects another update channel, and
both leave Goal data untouched. A channel with no newer build says so and
leaves the CLI choice standing. Explicit `LOOPX_BIN` overrides are retained and
are never replaced automatically: a mismatched override must be corrected by
its owner.

The installer and App-owned services use the same bounded tool search,
including standard Homebrew locations on macOS, without loading interactive
Expand Down
19 changes: 19 additions & 0 deletions apps/desktop/loopx-control-plane/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ fn maintenance_origin(url: &Url) -> String {
// echo verbatim into the boot surface, where it names the recovery panel's
// actionable diagnostics instead of a misleading fixed message.
fn boot_failure_message(error: &str) -> String {
// The pairing decision is not a failure: the window is waiting for the
// operator to choose between updating the App and aligning the CLI.
if error == "runtime_pairing_required" {
return "本机 LoopX 运行时与 App 自带的运行时不一致,请在上方选择「更新 App 与运行时」或「回退 CLI 到本 App 版本」后继续。"
.to_string();
}
let is_stable_code = !error.is_empty()
&& error.chars().all(|character| {
character.is_ascii_lowercase() || character.is_ascii_digit() || character == '_'
Expand Down Expand Up @@ -219,9 +225,16 @@ mod tests {
assert!(style.contains("@keyframes mark-breathe"));
assert!(style.contains("prefers-reduced-motion: reduce"));
assert!(style.contains("main[data-state=\"error\"] .progress::after"));
assert!(style.contains("main[data-state=\"decision\"] .progress"));
assert!(style.contains("--warning: #f5a623"));
assert!(script.contains("desktop_update_status"));
assert!(script.contains("window.loopxBootRetrying"));
// The first screen must offer both operator choices, not a repair path
// that silently replaces the CLI runtime.
assert!(html.contains("id=\"pairing-align\""));
assert!(html.contains("回退 CLI"));
assert!(script.contains("runtime_pairing_required"));
assert!(script.contains("\"align_runtime\""));
// The boot surface must derive its error projection from the polled
// snapshot itself and name the known fresh-Mac installer failure.
assert!(script.contains("runtime_install_exit_2"));
Expand All @@ -231,6 +244,12 @@ mod tests {
#[test]
fn boot_failure_message_appends_stable_codes_only() {
use super::boot_failure_message;
// The pairing decision names both operator choices instead of the
// generic startup failure text.
let pairing = boot_failure_message("runtime_pairing_required");
assert!(pairing.contains("更新 App 与运行时"));
assert!(pairing.contains("回退 CLI 到本 App 版本"));
assert!(!pairing.contains("错误码"));
assert_eq!(
boot_failure_message("runtime_install_exit_2"),
"本地服务暂时无法启动,请检查安装或端口占用。(错误码 runtime_install_exit_2,详见恢复与更新面板)"
Expand Down
Loading
Loading