@@ -20,8 +20,9 @@ use std::time::Duration;
2020use anyhow:: { Context , Result } ;
2121use rocm_core:: { AppPaths , RocmCliConfig , builtin_model_recipes, builtin_watchers} ;
2222use rocm_dash_daemon:: runner:: RunnerOptions ;
23- use rocm_dash_tui:: app:: { ActiveTab , ResolvedArgs } ;
23+ use rocm_dash_tui:: app:: { ActiveTab , Focus , ResolvedArgs } ;
2424use rocm_dash_tui:: ui:: automations_manager:: AutomationSummary ;
25+ use rocm_dash_tui:: ui:: launcher:: LauncherChoice ;
2526use rocm_dash_tui:: ui:: model_picker:: ModelRecipeSummary ;
2627use 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.
249277pub 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.
288325pub 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
304335async 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