Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
feat(tui): autoplay full update experience simulator
  • Loading branch information
1jehuang committed Aug 25, 2026
commit 3f199fa0f156a0177252e19a7df2e62ad513022f
3 changes: 3 additions & 0 deletions crates/jcode-tui/src/tui/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ mod shortcut_hints;
mod split_view;
mod state_ui;
mod state_ui_input_helpers;
mod update_sim;
pub(crate) use state_ui_input_helpers::registered_command_entries;
mod state_ui_maintenance;
mod state_ui_messages;
Expand Down Expand Up @@ -1075,6 +1076,8 @@ pub struct App {
/// simulator seeds synthetic phases so a developer can step through every
/// first-run screen via Alt+5 reset or Cmd+5 toggle without touching real auth state.
onboarding_sim: Option<usize>,
/// Active time-based, non-destructive update experience preview.
update_sim: Option<update_sim::UpdateSimulator>,
/// Active guided first-run onboarding flow (model select -> continue ->
/// transcript pick -> suggestions). `None` when not onboarding.
onboarding_flow: Option<onboarding_flow::OnboardingFlow>,
Expand Down
1 change: 1 addition & 0 deletions crates/jcode-tui/src/tui/app/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ pub(super) fn handle_tick(app: &mut App) -> bool {
needs_redraw |= app.poll_session_picker_load();
needs_redraw |= app.poll_session_picker_presence();
needs_redraw |= app.onboarding_tick();
needs_redraw |= app.progress_update_simulator();
needs_redraw |= app.poll_compaction_completion();
needs_redraw |= app.maybe_refresh_overnight_display_card();
needs_redraw |= super::commands::poll_local_transfer_prepare(app);
Expand Down
1 change: 1 addition & 0 deletions crates/jcode-tui/src/tui/app/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ pub(super) async fn handle_tick(app: &mut App, remote: &mut RemoteConnection) ->
needs_redraw |= app.poll_session_picker_load();
needs_redraw |= app.poll_session_picker_presence();
needs_redraw |= app.onboarding_tick();
needs_redraw |= app.progress_update_simulator();
needs_redraw |= app.refresh_keybindings_if_config_reloaded();

let _ = check_debug_command(app, remote).await;
Expand Down
5 changes: 1 addition & 4 deletions crates/jcode-tui/src/tui/app/state_ui_input_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,10 +184,7 @@ const REGISTERED_COMMANDS: &[RegisteredCommand] = &[
RegisteredCommand::public("/rebuild", "Background rebuild and auto reload"),
RegisteredCommand::public("/selfdev", "Open a new self-dev jcode session"),
RegisteredCommand::public("/update", "Background update and auto reload"),
RegisteredCommand::public(
"/update-sim",
"Preview update UI safely (Alt+_)",
),
RegisteredCommand::public("/update-sim", "Preview update UI safely (Alt+_)"),
RegisteredCommand::public("/resume", "Open session picker"),
RegisteredCommand::public("/sessions", "Alias for /resume"),
RegisteredCommand::public("/session", "Alias for /resume"),
Expand Down
9 changes: 1 addition & 8 deletions crates/jcode-tui/src/tui/app/state_ui_maintenance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,7 @@ impl App {
{
return false;
}
self.handle_update_status(crate::bus::UpdateStatus::Available {
current: jcode_build_meta::version().to_string(),
latest: "v99.0.0-simulated".to_string(),
});
self.push_display_message(DisplayMessage::system(
"Update simulator opened with Alt+_. Try `/update-sim download`, then `install`, `done`, or `error`. Nothing real will be changed."
.to_string(),
));
self.restart_update_simulator();
true
}

Expand Down
2 changes: 2 additions & 0 deletions crates/jcode-tui/src/tui/app/tui_lifecycle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,7 @@ impl App {
startup_submit_deferred_reason: None,
onboarding_preview_mode: false,
onboarding_sim: None,
update_sim: None,
onboarding_flow: None,
onboarding_auto_model_selection_active: Arc::new(AtomicBool::new(false)),
onboarding_auto_model_selection_baseline: Arc::new(std::sync::Mutex::new(None)),
Expand Down Expand Up @@ -945,6 +946,7 @@ impl App {
startup_submit_deferred_reason: None,
onboarding_preview_mode: false,
onboarding_sim: None,
update_sim: None,
onboarding_flow: None,
onboarding_auto_model_selection_active: Arc::new(AtomicBool::new(false)),
onboarding_auto_model_selection_baseline: Arc::new(std::sync::Mutex::new(None)),
Expand Down
10 changes: 9 additions & 1 deletion crates/jcode-tui/src/tui/app/tui_lifecycle_runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,15 @@ pub(super) fn handle_dev_command(app: &mut App, trimmed: &str) -> bool {
let stage = trimmed.strip_prefix("/update-sim").unwrap_or("").trim();
let simulated_version = "v99.0.0-simulated";
let status = match stage {
"" | "available" => crate::bus::UpdateStatus::Available {
"" => {
app.restart_update_simulator();
app.push_display_message(DisplayMessage::system(
"Update simulator started. It will automatically play the complete receive → download → install → restart experience. Press Alt+_ anytime to replay. Nothing real will be changed."
.to_string(),
));
return true;
}
"available" => crate::bus::UpdateStatus::Available {
current: jcode_build_meta::version().to_string(),
latest: simulated_version.to_string(),
},
Expand Down
106 changes: 106 additions & 0 deletions crates/jcode-tui/src/tui/app/update_sim.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
//! Non-destructive, time-based preview of receiving and installing an update.

use super::{App, DisplayMessage};
use crate::bus::UpdateStatus;
use std::time::{Duration, Instant};

pub(super) struct UpdateSimulator {
started_at: Instant,
stage: u8,
}

impl App {
/// Start from the normal TUI and autoplay the same status transitions used by
/// a real background update. No updater, download, install, or reload runs.
pub fn start_update_simulator_on_launch(&mut self) {
self.onboarding_flow = None;
self.onboarding_startup_checked = true;
self.onboarding_preview_mode = false;
self.update_sim = Some(UpdateSimulator {
started_at: Instant::now(),
stage: 0,
});
self.set_status_notice("Update simulator · waiting for update signal...");
self.force_full_redraw = true;
}

pub(super) fn restart_update_simulator(&mut self) {
self.remove_simulated_update_messages();
self.start_update_simulator_on_launch();
}

fn remove_simulated_update_messages(&mut self) {
let before = self.display_messages.len();
self.display_messages.retain(|message| {
message.title.as_deref() != Some("Updating jcode")
&& !message.content.starts_with("Update simulator complete")
});
if self.display_messages.len() != before {
self.bump_display_messages_version();
}
self.background_client_action = None;
self.pending_background_client_reload = None;
}

/// Advance the preview from the regular TUI tick. Delays are intentionally
/// long enough to see and critique each production UI state.
pub(super) fn progress_update_simulator(&mut self) -> bool {
let Some(sim) = self.update_sim.as_mut() else {
return false;
};
let elapsed = sim.started_at.elapsed();
let next_stage = match sim.stage {
0 if elapsed >= Duration::from_millis(1200) => 1,
1 if elapsed >= Duration::from_millis(2600) => 2,
2 if elapsed >= Duration::from_millis(3900) => 3,
3 if elapsed >= Duration::from_millis(5200) => 4,
4 if elapsed >= Duration::from_millis(6500) => 5,
5 if elapsed >= Duration::from_millis(8000) => 6,
6 if elapsed >= Duration::from_millis(9800) => 7,
7 if elapsed >= Duration::from_millis(11800) => 8,
_ => return false,
};
sim.stage = next_stage;

let version = "v99.0.0-simulated".to_string();
match next_stage {
1 => self.handle_update_status(UpdateStatus::Available {
current: jcode_build_meta::version().to_string(),
latest: version,
}),
2 => self.handle_update_status(UpdateStatus::Downloading {
version,
downloaded: 8 * 1_048_576,
total: Some(100 * 1_048_576),
}),
3 => self.handle_update_status(UpdateStatus::Downloading {
version,
downloaded: 37 * 1_048_576,
total: Some(100 * 1_048_576),
}),
4 => self.handle_update_status(UpdateStatus::Downloading {
version,
downloaded: 76 * 1_048_576,
total: Some(100 * 1_048_576),
}),
5 => self.handle_update_status(UpdateStatus::Downloading {
version,
downloaded: 100 * 1_048_576,
total: Some(100 * 1_048_576),
}),
6 => self.handle_update_status(UpdateStatus::Installing { version }),
7 => self.handle_update_status(UpdateStatus::Installed { version }),
8 => {
self.handle_update_status(UpdateStatus::UpToDate);
self.set_status_notice("Update simulator complete · restart successful");
self.push_display_message(DisplayMessage::system(
"Update simulator complete. This is the post-restart TUI; no files were changed. Press Alt+_ to replay."
.to_string(),
));
self.update_sim = None;
}
_ => {}
}
true
}
}
5 changes: 5 additions & 0 deletions src/cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ pub(crate) struct Args {
#[arg(long = "onboarding-sim")]
pub(crate) onboarding_sim: bool,

/// Launch the normal TUI, skip onboarding, then autoplay a safe simulation
/// of receiving, downloading, installing, and restarting after an update.
#[arg(long = "update-sim")]
pub(crate) update_sim: bool,

/// Custom socket path for server/client communication
#[arg(long, global = true)]
pub(crate) socket: Option<String>,
Expand Down
1 change: 1 addition & 0 deletions src/cli/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,7 @@ async fn run_default_command(args: Args) -> Result<()> {
args.fresh_spawn,
args.remote_working_dir,
args.onboarding_sim,
args.update_sim,
)
.await?;

Expand Down
12 changes: 10 additions & 2 deletions src/cli/selfdev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,16 @@ pub async fn run_self_dev(should_build: bool, resume_session: Option<String>) ->

output::stderr_info("Starting self-dev TUI...");

super::tui_launch::run_tui_client(Some(session_id), None, !server_running, false, None, false)
.await
super::tui_launch::run_tui_client(
Some(session_id),
None,
!server_running,
false,
None,
false,
false,
)
.await
}
#[cfg(test)]
#[path = "selfdev_tests.rs"]
Expand Down
4 changes: 4 additions & 0 deletions src/cli/tui_launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ pub async fn run_tui_client(
fresh_spawn: bool,
remote_working_dir: Option<String>,
onboarding_sim: bool,
update_sim: bool,
) -> Result<()> {
startup_profile::mark("tui_client_enter");
let (terminal, tui_runtime) = init_tui_runtime()?;
Expand Down Expand Up @@ -135,6 +136,9 @@ pub async fn run_tui_client(
if onboarding_sim {
app.start_onboarding_simulator_on_launch();
}
if update_sim {
app.start_update_simulator_on_launch();
}
startup_profile::mark("app_new_for_remote");
if resume_session.is_none()
&& let Some(hints) = startup_hints
Expand Down
Loading