Skip to content

Commit 5b32c86

Browse files
feat(dash): make the ROCm/Serving detail pane state-aware (#93)
The Actions -> Details pane rendered fixed copy. Thread `&AppState` into `pane::draw`/`draw_detail` and render a live status block per verb: - Set up / Install ROCm: detected ROCm + driver versions and installed runtimes - Serve a model: the known-model catalog - Engines: Lemonade install status (vLLM deferred until the daemon surfaces it) - Running instances: live instances inline (model/port/gen tps) — no popup Falls back to honest "not detected" / "none running" when telemetry is absent. Adds tab tests for the populated install + running-instances paths. Signed-off-by: Michael Roy <michael.roy@amd.com>
1 parent 6c975c5 commit 5b32c86

3 files changed

Lines changed: 222 additions & 19 deletions

File tree

crates/rocm-dash-tui/src/ui/tabs/pane.rs

Lines changed: 167 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@ use ratatui::style::{Modifier, Style};
2020
use ratatui::text::{Line, Span};
2121
use ratatui::widgets::{Paragraph, Wrap};
2222

23-
use crate::app::{KeyAction, PaneFocus};
23+
use rocm_dash_core::metrics::InstanceStatus;
24+
25+
use crate::app::{AppState, KeyAction, PaneFocus};
26+
use crate::ui::format;
2427
use crate::ui::panel::{self, BoxRole};
2528
use crate::ui::theme::Theme;
2629

@@ -82,12 +85,13 @@ pub fn draw(
8285
list_title: &str,
8386
verbs: &[Verb],
8487
sel: usize,
85-
focus: PaneFocus,
88+
state: &AppState,
8689
theme: &Theme,
8790
) {
91+
let focus = state.pane_focus;
8892
let cols = split_columns(area);
8993
draw_verb_list(f, cols[0], list_title, verbs, sel, focus, theme);
90-
draw_detail(f, cols[1], verbs, sel, focus, theme);
94+
draw_detail(f, cols[1], verbs, sel, focus, state, theme);
9195
}
9296

9397
/// Map a left-click in a domain tab's body to an action.
@@ -196,6 +200,7 @@ fn draw_detail(
196200
verbs: &[Verb],
197201
sel: usize,
198202
focus: PaneFocus,
203+
state: &AppState,
199204
theme: &Theme,
200205
) {
201206
let sel = sel.min(verbs.len().saturating_sub(1));
@@ -215,13 +220,22 @@ fn draw_detail(
215220
let mut lines: Vec<Line> = vec![
216221
Line::from(Span::styled(v.summary, Style::default().fg(theme.fg))),
217222
Line::from(""),
218-
Line::from(Span::styled(
219-
"What you'll do",
220-
Style::default()
221-
.fg(theme.muted)
222-
.add_modifier(Modifier::BOLD),
223-
)),
224223
];
224+
225+
// State-aware block: what's actually installed / known / running for this
226+
// verb, so the pane reflects the system rather than fixed copy.
227+
let live = live_lines(v.action, state, theme);
228+
if !live.is_empty() {
229+
lines.extend(live);
230+
lines.push(Line::from(""));
231+
}
232+
233+
lines.push(Line::from(Span::styled(
234+
"What you'll do",
235+
Style::default()
236+
.fg(theme.muted)
237+
.add_modifier(Modifier::BOLD),
238+
)));
225239
for (i, step) in v.steps.iter().enumerate() {
226240
lines.push(Line::from(vec![
227241
Span::styled(
@@ -281,3 +295,147 @@ fn draw_detail(
281295

282296
f.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
283297
}
298+
299+
/// State-aware "current status" lines for a verb, or empty when the verb has no
300+
/// live view.
301+
///
302+
/// Surfaces what's actually installed (ROCm/engines), known (model catalog), or
303+
/// running (instances) so the detail pane reflects the system — including the
304+
/// running-instances view inline, without opening the services popup.
305+
fn live_lines(action: KeyAction, state: &AppState, theme: &Theme) -> Vec<Line<'static>> {
306+
let head = |t: String| {
307+
Line::from(Span::styled(
308+
t,
309+
Style::default()
310+
.fg(theme.muted)
311+
.add_modifier(Modifier::BOLD),
312+
))
313+
};
314+
let mark = |ok: bool| {
315+
if ok {
316+
Span::styled("✓ ", Style::default().fg(theme.ok))
317+
} else {
318+
Span::styled("· ", Style::default().fg(theme.muted))
319+
}
320+
};
321+
let info = || {
322+
state
323+
.latest
324+
.as_ref()
325+
.and_then(|s| s.gpu_system_info.as_ref())
326+
};
327+
328+
match action {
329+
KeyAction::OpenInstall => {
330+
let mut lines = vec![head("Installed now".into())];
331+
let rocm = info().and_then(|i| i.rocm_version.clone());
332+
lines.push(Line::from(vec![
333+
mark(rocm.is_some()),
334+
Span::styled(
335+
rocm.map_or_else(|| "ROCm — not detected".into(), |v| format!("ROCm {v}")),
336+
Style::default().fg(theme.fg),
337+
),
338+
]));
339+
let driver = info().and_then(|i| i.driver_version.clone());
340+
lines.push(Line::from(vec![
341+
mark(driver.is_some()),
342+
Span::styled(
343+
driver.map_or_else(|| "Driver —".into(), |v| format!("Driver {v}")),
344+
Style::default().fg(theme.fg),
345+
),
346+
]));
347+
if !state.runtimes.is_empty() {
348+
lines.push(head(format!("{} runtime(s)", state.runtimes.len())));
349+
for rt in state.runtimes.iter().take(4) {
350+
lines.push(Line::from(vec![
351+
Span::styled(
352+
if rt.active { "● " } else { " " },
353+
Style::default().fg(theme.ok),
354+
),
355+
Span::styled(
356+
format!("{} ({} {})", rt.key, rt.channel, rt.version),
357+
Style::default().fg(theme.fg),
358+
),
359+
]));
360+
}
361+
}
362+
lines
363+
}
364+
KeyAction::OpenEngineManager => {
365+
let mut lines = vec![head("Engines".into())];
366+
let lemon = info().and_then(|i| i.lemond_version.clone());
367+
lines.push(Line::from(vec![
368+
mark(lemon.is_some()),
369+
Span::styled(
370+
lemon.map_or_else(
371+
|| "Lemonade — not installed".into(),
372+
|v| format!("Lemonade {v}"),
373+
),
374+
Style::default().fg(theme.fg),
375+
),
376+
]));
377+
// The daemon doesn't surface vLLM detection yet — open the manager to
378+
// check rather than claiming a status we don't have.
379+
lines.push(Line::from(vec![
380+
Span::styled("· ", Style::default().fg(theme.muted)),
381+
Span::styled("vLLM — open to check", Style::default().fg(theme.muted)),
382+
]));
383+
lines
384+
}
385+
KeyAction::OpenServeWizard => {
386+
let n = state.model_recipes.len();
387+
if n == 0 {
388+
return Vec::new();
389+
}
390+
let mut lines = vec![head(format!("Model catalog — {n} known"))];
391+
for r in state.model_recipes.iter().take(4) {
392+
lines.push(Line::from(vec![
393+
Span::styled("• ", Style::default().fg(theme.accent_2)),
394+
Span::styled(r.id.clone(), Style::default().fg(theme.fg)),
395+
]));
396+
}
397+
if n > 4 {
398+
lines.push(Line::from(Span::styled(
399+
format!(" …and {} more", n - 4),
400+
Style::default().fg(theme.muted),
401+
)));
402+
}
403+
lines
404+
}
405+
KeyAction::OpenServices => {
406+
let running: Vec<_> = state
407+
.instances
408+
.values()
409+
.filter(|i| i.status == InstanceStatus::Running)
410+
.collect();
411+
let mut lines = vec![head(format!("Running now — {}", running.len()))];
412+
if running.is_empty() {
413+
lines.push(Line::from(Span::styled(
414+
" none running",
415+
Style::default().fg(theme.muted),
416+
)));
417+
} else {
418+
for i in running.iter().take(5) {
419+
let port = i.port.map_or_else(String::new, |p| format!(" :{p}"));
420+
lines.push(Line::from(vec![
421+
Span::styled("● ", Style::default().fg(theme.ok)),
422+
Span::styled(i.model_name.clone(), Style::default().fg(theme.fg)),
423+
Span::styled(port, Style::default().fg(theme.muted)),
424+
Span::styled(
425+
format!(" gen {}", format::tps_opt(i.gen_tps)),
426+
Style::default().fg(theme.muted),
427+
),
428+
]));
429+
}
430+
if running.len() > 5 {
431+
lines.push(Line::from(Span::styled(
432+
format!(" …and {} more", running.len() - 5),
433+
Style::default().fg(theme.muted),
434+
)));
435+
}
436+
}
437+
lines
438+
}
439+
_ => Vec::new(),
440+
}
441+
}

crates/rocm-dash-tui/src/ui/tabs/rocm.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -123,15 +123,7 @@ pub fn hit_test(area: Rect, x: u16, y: u16) -> Option<KeyAction> {
123123
}
124124

125125
pub fn draw(f: &mut Frame, area: Rect, state: &AppState, theme: &Theme) {
126-
pane::draw(
127-
f,
128-
area,
129-
"ROCm actions",
130-
VERBS,
131-
state.rocm_sel,
132-
state.pane_focus,
133-
theme,
134-
);
126+
pane::draw(f, area, "ROCm actions", VERBS, state.rocm_sel, state, theme);
135127
}
136128

137129
#[cfg(test)]
@@ -171,6 +163,30 @@ mod tests {
171163
}
172164
}
173165

166+
#[test]
167+
fn rocm_detail_shows_installed_rocm_version() {
168+
use rocm_dash_core::metrics::{GpuSystemInfo, Snapshot};
169+
let mut s = AppState::new("t".into(), "default-dark".into());
170+
s.active_tab = ActiveTab::Rocm;
171+
s.rocm_sel = 0; // "Set up / Install ROCm" → OpenInstall
172+
s.latest = Some(Snapshot {
173+
gpu_system_info: Some(GpuSystemInfo {
174+
rocm_version: Some("6.4.1".into()),
175+
..Default::default()
176+
}),
177+
..Default::default()
178+
});
179+
let out = render(&s, 120, 28);
180+
assert!(
181+
out.contains("Installed now"),
182+
"live install header missing: {out:?}"
183+
);
184+
assert!(
185+
out.contains("6.4.1"),
186+
"detected ROCm version not shown: {out:?}"
187+
);
188+
}
189+
174190
#[test]
175191
fn rocm_verb_action_maps_selection_to_seam() {
176192
assert_eq!(verb_action(0), KeyAction::OpenInstall);

crates/rocm-dash-tui/src/ui/tabs/serving.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ pub fn draw(f: &mut Frame, area: Rect, state: &AppState, theme: &Theme) {
130130
"Serving actions",
131131
VERBS,
132132
state.serving_sel,
133-
state.pane_focus,
133+
state,
134134
theme,
135135
);
136136
}
@@ -177,6 +177,35 @@ mod tests {
177177
assert!(out.contains("soon"), "optimize soon badge missing: {out:?}");
178178
}
179179

180+
#[test]
181+
fn serving_detail_lists_running_instances_inline() {
182+
use rocm_dash_core::metrics::{Instance, InstanceStatus};
183+
let mut s = AppState::new("t".into(), "default-dark".into());
184+
s.active_tab = ActiveTab::Serving;
185+
s.serving_sel = 2; // "Running instances/services" → OpenServices
186+
s.instances.insert(
187+
"vllm-1".into(),
188+
Instance {
189+
container_name: "vllm-1".into(),
190+
model_name: "Llama-3.1-8B".into(),
191+
status: InstanceStatus::Running,
192+
port: Some(8000),
193+
..Default::default()
194+
},
195+
);
196+
let out = render(&s, 120, 28);
197+
// The running instance is shown inline in the detail pane — no popup.
198+
assert!(
199+
out.contains("Running now"),
200+
"live running header missing: {out:?}"
201+
);
202+
assert!(
203+
out.contains("Llama-3.1-8B"),
204+
"running model not shown inline: {out:?}"
205+
);
206+
assert!(out.contains("8000"), "running port not shown: {out:?}");
207+
}
208+
180209
#[test]
181210
fn serving_verb_action_maps_selection_to_seam() {
182211
assert_eq!(verb_action(0), KeyAction::OpenServeWizard);

0 commit comments

Comments
 (0)