Skip to content

Commit 800785e

Browse files
feat(launcher): make the bare-rocm launcher self-contained (#78)
Run Set up / Serve / Diagnose to completion in place instead of bouncing into the full Dash. The launcher is now a persistent hub: each flow returns to the menu on exit; only Chat and "Open full dashboard" escalate into the full Dash. Design: a focused host reuses app::run/event_loop via a default-off ResolvedArgs.focus: Option<Focus>. When focus is set the loop opens exactly one overlay, skips the embedded daemon + chat backend, renders overlay-only (ui::draw_focused), and exits at root back to the menu. focus=None is byte-identical to the existing dashboard/chat. - Reorder rows: Set up this system (0), Serve a model (1) - Diagnose runs read-only `rocm examine` in place (auto-runs on open) - rocm bootstrap setup routes to the same focused Setup host - Fold out start_onboarding (subsumed by Focus::Setup) - Skip the embedded daemon in focused mode (should_spawn_daemon) - Guard against mid-job ejection: in focused mode, swallow the console close keys (q / running-Esc) while a job is non-terminal so the overlay is never nulled mid-run (which would kill the child via kill_on_drop); Ctrl+C still cancels, terminal jobs exit normally Coverage 84% on the rocm-dash crates; full workspace build/test/clippy/ hawkeye green. Signed-off-by: Michael Roy <michael.roy@amd.com>
1 parent be23215 commit 800785e

5 files changed

Lines changed: 608 additions & 71 deletions

File tree

apps/rocm/src/dash.rs

Lines changed: 123 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ use std::time::Duration;
2020
use anyhow::{Context, Result};
2121
use rocm_core::{AppPaths, RocmCliConfig, builtin_model_recipes, builtin_watchers};
2222
use rocm_dash_daemon::runner::RunnerOptions;
23-
use rocm_dash_tui::app::{ActiveTab, ResolvedArgs};
23+
use rocm_dash_tui::app::{ActiveTab, Focus, ResolvedArgs};
2424
use rocm_dash_tui::ui::automations_manager::AutomationSummary;
25+
use rocm_dash_tui::ui::launcher::LauncherChoice;
2526
use rocm_dash_tui::ui::model_picker::ModelRecipeSummary;
2627
use rocm_dash_tui::ui::runtime_manager::RuntimeSummary;
2728

@@ -182,7 +183,8 @@ pub fn resolved_args(
182183
theme: t.theme.clone(),
183184
replay: None,
184185
initial_tab,
185-
start_onboarding: false,
186+
// Default: not a focused host. `run_focused` sets this per launcher flow.
187+
focus: None,
186188
chat_url: t.chat_url.clone(),
187189
chat_model: t.chat_model.clone(),
188190
chat_auth_header: t.chat_auth_header.clone(),
@@ -240,34 +242,70 @@ pub fn run(replay: Option<PathBuf>, demo: bool, chat_mock: bool) -> Result<()> {
240242
rt.block_on(run_async(config, paths, args, replay, chat_mock))
241243
}
242244

243-
/// Entry point for bare `rocm`: show the minimal launcher front door, then
244-
/// escalate into the existing dash/chat entry points based on the choice.
245+
/// Where a launcher choice leads. Pure mapping so the hub-loop body stays
246+
/// trivial and the routing is unit-testable without a terminal.
247+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
248+
enum LauncherRoute {
249+
/// Run in place via the focused host (Set up / Serve / Diagnose).
250+
Focused(Focus),
251+
/// Escalate into the full dashboard with the Chat tab focused.
252+
Chat,
253+
/// Escalate into the full dashboard (Home).
254+
Dashboard,
255+
}
256+
257+
/// Map a launcher row to its destination. Set up / Serve / Diagnose run in place
258+
/// (focused host); Chat and Open-dashboard are the only escalations into the
259+
/// full Dash.
260+
const fn launcher_route(choice: LauncherChoice) -> LauncherRoute {
261+
match choice {
262+
LauncherChoice::SetUp => LauncherRoute::Focused(Focus::Setup),
263+
LauncherChoice::Serve => LauncherRoute::Focused(Focus::Serve),
264+
LauncherChoice::Diagnose => LauncherRoute::Focused(Focus::Examine),
265+
LauncherChoice::Chat => LauncherRoute::Chat,
266+
LauncherChoice::OpenDashboard => LauncherRoute::Dashboard,
267+
}
268+
}
269+
270+
/// Entry point for bare `rocm`: the launcher as a persistent hub.
245271
///
246-
/// The launcher is a thin synchronous pre-screen (no daemon, no async runtime);
247-
/// `rocm dash` / `rocm chat` reach [`run`] / [`run_chat`] directly and are
248-
/// unaffected by this path.
272+
/// Draws the minimal front door; runs the chosen flow; then redraws the menu so
273+
/// the user can run several flows without relaunching. Set up / Serve / Diagnose
274+
/// run in place via the focused host ([`run_focused`]); Chat and Open-dashboard
275+
/// escalate into the full Dash. `q`/`Esc` (the `None` choice) leaves. A flow
276+
/// error breaks the loop and propagates.
249277
pub fn run_launcher(chat_mock: bool) -> Result<()> {
250278
let paths = AppPaths::discover()?;
251279
let config = RocmCliConfig::load(&paths).unwrap_or_default();
252280
let theme = config.dashboard.tui.theme;
253-
match rocm_dash_tui::ui::launcher::run_launcher(&theme)? {
254-
None => Ok(()),
255-
Some(choice) => {
256-
use rocm_dash_tui::ui::launcher::LauncherChoice;
257-
match choice {
258-
LauncherChoice::Chat => run_chat(chat_mock),
259-
// Serve / Set up / Diagnose / Open dashboard all escalate into
260-
// the full dash (Home); the guided managers are reachable there
261-
// via the Action tab + seam (ponytail: no separate routing yet).
262-
LauncherChoice::Serve
263-
| LauncherChoice::SetUp
264-
| LauncherChoice::Diagnose
265-
| LauncherChoice::OpenDashboard => run(None, false, chat_mock),
266-
}
281+
loop {
282+
match rocm_dash_tui::ui::launcher::run_launcher(&theme)? {
283+
None => return Ok(()),
284+
Some(choice) => match launcher_route(choice) {
285+
LauncherRoute::Focused(focus) => run_focused(focus)?,
286+
LauncherRoute::Chat => run_chat(chat_mock)?,
287+
LauncherRoute::Dashboard => run(None, false, chat_mock)?,
288+
},
267289
}
268290
}
269291
}
270292

293+
/// Entry point for a focused launcher flow (Set up / Serve / Diagnose).
294+
///
295+
/// Opens the dashboard runtime hosting exactly the one overlay for `focus` — no
296+
/// embedded daemon (see [`should_spawn_daemon`]), no tab shell — and returns to
297+
/// the launcher when that overlay is closed at its root. The keyring lookup in
298+
/// [`resolved_args`] runs here on the synchronous thread, before the runtime
299+
/// (nested-runtime invariant).
300+
pub fn run_focused(focus: Focus) -> Result<()> {
301+
let paths = AppPaths::discover()?;
302+
let config = RocmCliConfig::load(&paths)?;
303+
let mut args = resolved_args(&config, &paths, ActiveTab::Home);
304+
args.focus = Some(focus);
305+
let rt = build_dashboard_runtime()?;
306+
rt.block_on(run_async(config, paths, args, None, false))
307+
}
308+
271309
/// Entry point for interactive `rocm chat`. Opens the unified dashboard with the
272310
/// Chat tab focused. Thin wrapper over the same runtime/`run_async` path as
273311
/// [`run`]; no replay/demo, embedded daemon as usual.
@@ -281,24 +319,17 @@ pub fn run_chat(chat_mock: bool) -> Result<()> {
281319
rt.block_on(run_async(config, paths, args, None, chat_mock))
282320
}
283321

284-
/// Entry point for `rocm bootstrap setup`. Opens the dashboard straight into the
285-
/// first-run onboarding wizard (install ROCm SDK / adopt an existing folder).
286-
/// Same runtime/`run_async` path as [`run`]; the keyring lookup in
287-
/// `resolved_args` runs here on the synchronous thread before the runtime.
322+
/// Entry point for `rocm bootstrap setup`. Routes to the same focused Setup host
323+
/// as the launcher's "Set up this system" row — the first-run onboarding wizard
324+
/// (install ROCm SDK / adopt an existing folder), with no daemon or tab shell.
288325
pub fn run_bootstrap() -> Result<()> {
289-
let paths = AppPaths::discover()?;
290-
let config = RocmCliConfig::load(&paths)?;
291-
let args = bootstrap_args(&config, &paths);
292-
let rt = build_dashboard_runtime()?;
293-
rt.block_on(run_async(config, paths, args, None, false))
326+
run_focused(bootstrap_focus())
294327
}
295328

296-
/// Resolve the dashboard args for `rocm bootstrap setup`: the standard
297-
/// dashboard args with the first-run onboarding wizard opened on launch.
298-
fn bootstrap_args(config: &RocmCliConfig, paths: &AppPaths) -> ResolvedArgs {
299-
let mut args = resolved_args(config, paths, ActiveTab::Home);
300-
args.start_onboarding = true;
301-
args
329+
/// The focused flow `rocm bootstrap setup` routes to — the onboarding host,
330+
/// identical to the launcher's "Set up this system".
331+
const fn bootstrap_focus() -> Focus {
332+
Focus::Setup
302333
}
303334

304335
async fn run_async(
@@ -320,9 +351,10 @@ async fn run_async(
320351
std::sync::Arc::new(crate::dash_seam::BinToolExecutor::new(paths.clone()));
321352
args.tool_executor = Some(executor);
322353
}
323-
// A live daemon is only needed when connecting; replay/demo feeds events
324-
// straight into the TUI, so skip the embedded daemon in that mode.
325-
let embedded = if replay.is_none() {
354+
// A live daemon is only needed for a connected full dashboard; replay/demo
355+
// feeds events straight into the TUI, and a focused host draws no telemetry
356+
// and streams its own job via the job-bridge — both skip the embedded daemon.
357+
let embedded = if should_spawn_daemon(&args) {
326358
maybe_spawn_embedded_daemon(&args.connect, &config, &paths).await
327359
} else {
328360
None
@@ -342,6 +374,16 @@ async fn run_async(
342374
result
343375
}
344376

377+
/// Whether `run_async` should auto-start the embedded telemetry daemon: only for
378+
/// a live, non-replay FULL dashboard. A focused host (`args.focus.is_some()`)
379+
/// draws no telemetry and streams its own job via the job-bridge, so it needs no
380+
/// daemon — and must not re-surface the socket-crash class for a flow that never
381+
/// uses it. Replay/demo (`args.replay.is_some()`) feeds events straight in.
382+
/// Pure predicate → unit-testable. Call after `args.replay` is set in `run_async`.
383+
const fn should_spawn_daemon(args: &ResolvedArgs) -> bool {
384+
args.replay.is_none() && args.focus.is_none()
385+
}
386+
345387
/// Auto-start an embedded telemetry daemon when no local one is already
346388
/// listening, so `rocm dash` works without a separate `rocm daemon` terminal.
347389
/// Returns the task handle + socket to clean up on exit, or `None` when an
@@ -416,24 +458,53 @@ mod tests {
416458
}
417459

418460
#[test]
419-
fn bootstrap_args_open_the_onboarding_wizard() {
420-
// `rocm bootstrap setup` must launch the dashboard straight into the
421-
// first-run onboarding overlay (install ROCm SDK / adopt existing).
422-
let args = bootstrap_args(&cfg(), &paths());
423-
assert!(
424-
args.start_onboarding,
425-
"bootstrap must open the onboarding wizard on launch"
461+
fn bootstrap_routes_to_focused_setup() {
462+
// `rocm bootstrap setup` must route to the same focused Setup host as the
463+
// launcher's "Set up this system" row (onboarding, no daemon/tab shell).
464+
assert_eq!(bootstrap_focus(), Focus::Setup);
465+
}
466+
467+
#[test]
468+
fn launcher_route_maps_every_choice() {
469+
// Set up / Serve / Diagnose run in place; Chat / Open-dashboard escalate.
470+
assert_eq!(
471+
launcher_route(LauncherChoice::SetUp),
472+
LauncherRoute::Focused(Focus::Setup)
473+
);
474+
assert_eq!(
475+
launcher_route(LauncherChoice::Serve),
476+
LauncherRoute::Focused(Focus::Serve)
477+
);
478+
assert_eq!(
479+
launcher_route(LauncherChoice::Diagnose),
480+
LauncherRoute::Focused(Focus::Examine),
481+
);
482+
assert_eq!(launcher_route(LauncherChoice::Chat), LauncherRoute::Chat);
483+
assert_eq!(
484+
launcher_route(LauncherChoice::OpenDashboard),
485+
LauncherRoute::Dashboard
426486
);
427-
assert_eq!(args.initial_tab, ActiveTab::Home);
428-
assert!(!args.chat_mock);
429-
assert!(args.replay.is_none());
430487
}
431488

432489
#[test]
433-
fn resolved_args_default_does_not_open_onboarding() {
434-
// Normal `rocm dash` / `rocm chat` must NOT auto-open onboarding.
490+
fn resolved_args_default_has_no_focus() {
491+
// Normal `rocm dash` / `rocm chat` are NOT focused hosts.
435492
let args = resolved_args(&cfg(), &paths(), ActiveTab::Home);
436-
assert!(!args.start_onboarding);
493+
assert!(args.focus.is_none());
494+
}
495+
496+
#[test]
497+
fn focused_and_replay_suppress_the_embedded_daemon() {
498+
// Full live dash → spawn the embedded daemon.
499+
let mut args = resolved_args(&cfg(), &paths(), ActiveTab::Home);
500+
assert!(should_spawn_daemon(&args), "full dash spawns the daemon");
501+
// Focused host → never spawn a daemon (avoids the socket-crash class).
502+
args.focus = Some(Focus::Examine);
503+
assert!(!should_spawn_daemon(&args), "focused host: no daemon");
504+
// Replay also suppresses it.
505+
args.focus = None;
506+
args.replay = Some(PathBuf::from("/tmp/x.ndjson"));
507+
assert!(!should_spawn_daemon(&args), "replay: no daemon");
437508
}
438509

439510
#[test]

0 commit comments

Comments
 (0)