Summary
The local-mode Activity panel and device list rendering have accumulated a handful of small but visible issues that together make the local view look broken on Apple Silicon. This issue tracks seven concrete fixes, most of them constrained to src/ui/activity_panel.rs, src/ui/gpu_sparkline_panel.rs, src/ui/process_renderer.rs, and the four device renderers under src/ui/renderers/.
The fixes are grouped in one issue because they are all noticed on the same screenshot, touch overlapping files, and are individually small enough that splitting them would create more overhead than value. They can be implemented as one PR (or split into 2–3 follow-up commits if the reviewer prefers).
Reproduction
Run ./target/release/all-smi view on an Apple M1 Ultra (16P+4E) / macOS. Observe the top Activity panel (CPU Cores + GPU Metrics), the GPU/CPU/Memory/Disk list rows, and the bottom "Processes:" header.
1. CPU Cores panel bottom border is off by one → also breaks the GPU Metrics panel
Where: src/ui/activity_panel.rs:210-219 (draw_panel_bottom_border).
Problem: The top border emits exactly panel_width display columns, and every content row emits exactly panel_width columns, but the bottom border emits panel_width + 1 columns.
Top border : " " + "╭─" + " " + title + " " + dashes + "╮"
= 2 + 2 + 1 + title.len() + 1 + (panel_width − title.len() − 7) + 1
= panel_width ← correct
Content row : " " + "│ " + bar_width + " " + blocks + pad + " │"
= panel_width ← correct
Bottom border : " " + "╰" + (inner_width + 1)·"─" + "╯"
= 2 + 1 + (panel_width − 4 + 1) + 1
= panel_width + 1 ← 1 CHAR TOO LONG
Because render_combined_activity_panel writes the pre-formatted left (CPU) lines before the right (GPU) lines on the same physical row, the 1-char overflow on the CPU bottom-border row pushes the GPU Metrics bottom-border row one column to the right. That is why both boxes appear broken on the bottom edge in the screenshot — the user originally described this as "the E-CPU section's border is off by one, which also breaks the CPU Core and GPU Metrics borders."
Fix:
// src/ui/activity_panel.rs, inside draw_panel_bottom_border
for _ in 0..inner_width { // was: 0..(inner_width + 1)
print_colored_text(stdout, "\u{2500}", Color::Cyan, None, None);
}
That gives 2 + 1 + inner_width + 1 = inner_width + 4 = panel_width — matching the top border and all content rows.
Regression test to add: Extend test_render_activity_panel_pe_cluster (or add a new test_activity_panel_border_widths_match) that strips ANSI escapes and asserts every line of the rendered panel has the same display-column count. This prevents the bug from silently reappearing.
2. P-CPU and E-CPU gauge widths differ within the same box
Where: src/ui/activity_panel.rs:349-437 (draw_pe_cluster_bars + draw_cluster_line).
Problem: draw_cluster_line recomputes bar_width per cluster as content_width − block_section_width − 2. Because the P-cluster has more cores than the E-cluster (16 vs. 4 on an M1 Ultra), block_section_width is much larger for P, so the P-CPU bar ends up ~15 columns shorter than the E-CPU bar. The result is two mismatched gauges stacked in the same box, which is visually confusing — the longer E-CPU bar implies higher capacity, which is the opposite of what's true.
Fix: Compute one shared bar_width in draw_pe_cluster_bars using the larger block section (i.e. the cluster with more cores → the shorter bar), then pass that pre-computed bar_width to both draw_cluster_line calls. Change draw_cluster_line's signature to accept bar_width: usize directly instead of recomputing from content_width + cores.len().
Sketch:
fn draw_pe_cluster_bars<W: Write>(
stdout: &mut W,
info: &CpuInfo,
panel_width: usize,
_full_width: usize,
) {
let Some(apple) = &info.apple_silicon_info else { return; };
let content_width = panel_width.saturating_sub(6);
let p_cores: Vec<&CoreUtilization> = /* existing filter */;
let e_cores: Vec<&CoreUtilization> = /* existing filter */;
// Shared bar width, aligned to the cluster with MORE cores (= the
// larger block section = the shorter bar). This makes the P-CPU and
// E-CPU gauges visually comparable instead of implying different
// capacities.
let p_block_width = p_cores.len() + (p_cores.len() / 4);
let e_block_width = e_cores.len() + (e_cores.len() / 4);
let shared_bar_width =
content_width.saturating_sub(p_block_width.max(e_block_width) + 2);
draw_cluster_line(stdout, "P-CPU", apple.p_core_utilization,
&p_cores, shared_bar_width, panel_width);
draw_cluster_line(stdout, "E-CPU", apple.e_core_utilization,
&e_cores, shared_bar_width, panel_width);
}
fn draw_cluster_line<W: Write>(
stdout: &mut W,
label: &str,
utilization: f64,
cores: &[&CoreUtilization],
bar_width: usize, // NEW: precomputed, shared across clusters
panel_width: usize,
) {
// border left → draw_bar(stdout, label, util, 100.0, bar_width, None)
// → space → per-core block loop → recompute blocks_printed as before
// → pad = panel_width − (4 + bar_width + 1 + blocks_printed) − 2
// → " │" border right
}
The total line length stays panel_width; the extra columns freed by the narrower E-CPU bar simply become extra right-side padding on the E-CPU row. Blocks remain left-aligned next to the bar so they read as an extension of the gauge.
3. GPU Temp row shows the "Nominal" thermal-pressure badge — remove it
Where: src/ui/gpu_sparkline_panel.rs:246-262 (build_rows) and :398-413 (thermal_pressure_badge).
Problem: On Apple Silicon, the GPU Temp row in the upper-right GPU Metrics panel is followed by a qualitative NSProcessInfo thermal pressure label ("Nominal" / "Fair" / "Serious" / "Critical"). This is the only row with a trailing badge, and it confuses the column layout (see issue 4 below). It is also not useful — the row already shows a real die temperature in °C since the SMC flt little-endian fix, and dashboard.rs only surfaces thermal_pressure in remote mode (the draw_system_view path), so removing it from the sparkline panel doesn't lose any information the user is actually using.
Fix: In build_rows, drop the thermal_badge computation for the GPU Temp row and set badge: None. The thermal_pressure_badge helper can be deleted entirely if no other caller exists (grep confirms this is the only call site). Also delete the now-dead test_thermal_pressure_badge_* tests and remove "thermal_pressure" entries from the make_apple_silicon_state fixture in the module's tests.
4. GPU Temp sparkline is visibly shorter than the rows above it
Where: src/ui/gpu_sparkline_panel.rs:331-340 (draw_sparkline_row).
Problem: draw_sparkline_row computes sparkline_width = content_width − fixed − badge_len. Because only the GPU Temp row has a badge, its sparkline is badge_len columns shorter than the GPU Util and GPU Mem rows above it. The total row width is still panel_width (the missing columns are absorbed into pad), but the visible sparkline column looks ragged — the right end of the GPU Temp sparkline does not line up with the sparklines above it.
Fix: This is fully resolved by fix #3. Once the thermal badge is removed, badge_len is 0 for every row and sparkline_width becomes identical across rows.
A belt-and-braces improvement (optional, can be a follow-up): make draw_sparkline_row compute sparkline_width once from the maximum badge_len across rows and reuse that width for every row. That future-proofs the layout for any new per-row badge without re-introducing the mismatch.
5. @ <hostname> is redundant on GPU / CPU / Host Memory / Disk rows in local mode
Where:
src/ui/renderers/gpu_renderer.rs (print_gpu_info, ~line 133)
src/ui/renderers/cpu_renderer.rs (print_cpu_info, ~line 263)
src/ui/renderers/memory_renderer.rs (print_memory_info, ~line 75)
src/ui/renderers/storage_renderer.rs (print_storage_info, ~line 95)
- Callers in
src/view/frame_renderer.rs (render_local_devices, render_remote_devices, render_gpu_section)
Problem: In local mode every device row shows … @ Cube.lo …, but there's only one node — the host name is already surfaced in the top local-header bar (Host Cube.local · Apple M1 Ultra …) and in the NODE chassis row. Repeating it four more times on every frame is noisy and wastes ~12 columns per row that could otherwise be used for a longer device name or just left empty to lighten the visual.
Fix: Add a show_hostname: bool parameter to each of the four print_*_info device renderers (NOT to print_chassis_info — the user explicitly kept the chassis NODE line in scope of the "@ hostname" hiding request, and the NODE line already uses a different format NODE <host> Pwr:… without @; leave it alone to keep this issue tightly scoped). When show_hostname is false, skip the @ hostname_display segment entirely — do not emit even a blank equivalent, so the rest of the line (Util:, VRAM:, etc.) shifts left to fill the gap.
Also skip looking up hostname_scroll_offset / invoking format_hostname_with_scroll on the hidden-hostname path, and skip advancing the hostname scroll animation in local mode from src/view/event_handler.rs (search for host_id_scroll_offsets) — no user-visible scrolling hostname means no reason to burn CPU incrementing the offset.
In src/view/frame_renderer.rs, pass show_hostname: !view_state.is_local_mode from the render_local_devices / render_gpu_section / render_chassis_section call sites. The remote-mode callers (render_remote_devices) keep show_hostname: true.
Update the per-module tests to cover both states (show_hostname = true retains the @ hostname substring in the stripped-ANSI output; show_hostname = false does not).
6. GPU Metrics sparkline panel doesn't show ANE even on Apple Silicon; add room for NPU on Intel
Where: src/ui/gpu_sparkline_panel.rs:71-79 (gpu_content_rows) and :215-300 (build_rows / has_ane_data).
Problem: has_ane_data currently returns false when gpu.ane_utilization == 0.0, so the ANE row is hidden whenever the Neural Engine is idle — which is most of the time on a desktop M1 Ultra, even though the hardware clearly has an ANE. The user wants the ANE row to be always present on Apple Silicon so the panel reflects the real hardware layout. (The individual GPU info row in print_gpu_info already draws an always-on ANE gauge at gpu_renderer.rs:311-330; this fix brings the top GPU Metrics panel into the same behaviour.)
The user also asked to leave room for a similar "NPU" row on Intel CPUs once NPU telemetry lands (Meteor Lake / Core Ultra). This doesn't require real data today — just the scaffolding so that adding an Intel NPU reader later is a one-line UI change.
Fix:
- Rename/repurpose
has_ane_data to show_ane_row, and let it return detect_apple_silicon(state) alone (drop the ane_utilization > 0.0 gate). Add a doc comment explaining that an ANE at 0 W is a meaningful "idle" reading and the row is load-bearing for platform identity even when idle.
- Update
gpu_content_rows to include the ANE row whenever show_ane_row is true — the row count becomes 5 on Apple Silicon regardless of current ANE power.
- Add a
show_npu_row(state: &AppState) -> bool helper right next to show_ane_row. For now it returns false (no Intel/Windows NPU reader exists yet), and a short doc comment points at src/api/metrics/npu/common.rs as the hand-off point for future NPU data. In build_rows, add a mirror of the ANE block that emits an "NPU" row (Yellow, same layout) when show_npu_row is true, so that enabling it later is literally flipping the helper return value.
- Update the existing
test_gpu_content_rows_apple_with_ane expectation from 5→5 (unchanged) and add test_gpu_content_rows_apple_without_ane_data_still_shows_row covering ane_utilization == 0.0.
As a follow-up (out of scope here), a dedicated ane_power_history: VecDeque<f64> on AppState would give the ANE sparkline real history instead of the current single-point vec![ane_w]. Worth mentioning in the issue body so the reviewer knows the sparkline trivialness is intentional, not forgotten.
7. Make the Processes: header look like the rest of the UI
Where: src/ui/process_renderer.rs:59.
Problem: Every other panel in the TUI uses either a full box (CPU Cores, GPU Metrics) or a colored horizontal rule. The process section opens with a bare ASCII literal Processes: followed directly by an un-labelled column header — it's the one unstyled artefact in the view and stands out as half-finished.
Fix: Replace the single queue!(stdout, Print("Processes:\r\n")) call with a one-line styled title rule that matches the rest of the UI, for example:
// Styled title line: "── Processes ───────────────────────"
print_colored_text(stdout, "\u{2500}\u{2500} ", Color::Cyan, None, None);
print_colored_text(stdout, "Processes", Color::Cyan, None, None);
print_colored_text(stdout, " ", Color::DarkGrey, None, None);
let title_prefix_cols = 3 + "Processes".len() + 1; // "── Processes "
let dashes = width.saturating_sub(title_prefix_cols);
print_colored_text(stdout, &"\u{2500}".repeat(dashes), Color::DarkGrey, None, None);
queue!(stdout, Print("\r\n")).unwrap();
Keep the line count unchanged (still exactly 1 row) so that RESERVED_HEADER_ROWS = 4 and the existing available_rows_for_processes math continues to work without adjustment. An optional enhancement: append a compact [N total · M active] counter at the end of the title rule, formatted in DarkGrey, to make the header carry actual information instead of just looking pretty — but that's optional and can be split out.
Acceptance Criteria
Non-goals
- No changes to the
NODE chassis line (keep chassis hostname display as-is).
- No new
ane_power_history on AppState — the ANE sparkline stays single-point for this issue.
- No actual Intel NPU reader implementation — only UI scaffolding.
- No changes to remote-mode rendering beyond what naturally falls out of the
show_hostname: bool parameter plumbing.
Summary
The local-mode Activity panel and device list rendering have accumulated a handful of small but visible issues that together make the local view look broken on Apple Silicon. This issue tracks seven concrete fixes, most of them constrained to
src/ui/activity_panel.rs,src/ui/gpu_sparkline_panel.rs,src/ui/process_renderer.rs, and the four device renderers undersrc/ui/renderers/.The fixes are grouped in one issue because they are all noticed on the same screenshot, touch overlapping files, and are individually small enough that splitting them would create more overhead than value. They can be implemented as one PR (or split into 2–3 follow-up commits if the reviewer prefers).
Reproduction
Run
./target/release/all-smi viewon an Apple M1 Ultra (16P+4E) / macOS. Observe the top Activity panel (CPU Cores + GPU Metrics), the GPU/CPU/Memory/Disk list rows, and the bottom "Processes:" header.1. CPU Cores panel bottom border is off by one → also breaks the GPU Metrics panel
Where:
src/ui/activity_panel.rs:210-219(draw_panel_bottom_border).Problem: The top border emits exactly
panel_widthdisplay columns, and every content row emits exactlypanel_widthcolumns, but the bottom border emitspanel_width + 1columns.Because
render_combined_activity_panelwrites the pre-formatted left (CPU) lines before the right (GPU) lines on the same physical row, the 1-char overflow on the CPU bottom-border row pushes the GPU Metrics bottom-border row one column to the right. That is why both boxes appear broken on the bottom edge in the screenshot — the user originally described this as "the E-CPU section's border is off by one, which also breaks the CPU Core and GPU Metrics borders."Fix:
That gives
2 + 1 + inner_width + 1 = inner_width + 4 = panel_width— matching the top border and all content rows.Regression test to add: Extend
test_render_activity_panel_pe_cluster(or add a newtest_activity_panel_border_widths_match) that strips ANSI escapes and asserts every line of the rendered panel has the same display-column count. This prevents the bug from silently reappearing.2. P-CPU and E-CPU gauge widths differ within the same box
Where:
src/ui/activity_panel.rs:349-437(draw_pe_cluster_bars+draw_cluster_line).Problem:
draw_cluster_linerecomputesbar_widthper cluster ascontent_width − block_section_width − 2. Because the P-cluster has more cores than the E-cluster (16 vs. 4 on an M1 Ultra),block_section_widthis much larger for P, so the P-CPU bar ends up ~15 columns shorter than the E-CPU bar. The result is two mismatched gauges stacked in the same box, which is visually confusing — the longer E-CPU bar implies higher capacity, which is the opposite of what's true.Fix: Compute one shared
bar_widthindraw_pe_cluster_barsusing the larger block section (i.e. the cluster with more cores → the shorter bar), then pass that pre-computedbar_widthto bothdraw_cluster_linecalls. Changedraw_cluster_line's signature to acceptbar_width: usizedirectly instead of recomputing fromcontent_width+cores.len().Sketch:
The total line length stays
panel_width; the extra columns freed by the narrower E-CPU bar simply become extra right-side padding on the E-CPU row. Blocks remain left-aligned next to the bar so they read as an extension of the gauge.3. GPU Temp row shows the "Nominal" thermal-pressure badge — remove it
Where:
src/ui/gpu_sparkline_panel.rs:246-262(build_rows) and:398-413(thermal_pressure_badge).Problem: On Apple Silicon, the GPU Temp row in the upper-right GPU Metrics panel is followed by a qualitative NSProcessInfo thermal pressure label ("Nominal" / "Fair" / "Serious" / "Critical"). This is the only row with a trailing badge, and it confuses the column layout (see issue 4 below). It is also not useful — the row already shows a real die temperature in °C since the SMC
fltlittle-endian fix, anddashboard.rsonly surfacesthermal_pressurein remote mode (thedraw_system_viewpath), so removing it from the sparkline panel doesn't lose any information the user is actually using.Fix: In
build_rows, drop thethermal_badgecomputation for theGPU Temprow and setbadge: None. Thethermal_pressure_badgehelper can be deleted entirely if no other caller exists (grep confirms this is the only call site). Also delete the now-deadtest_thermal_pressure_badge_*tests and remove"thermal_pressure"entries from themake_apple_silicon_statefixture in the module's tests.4. GPU Temp sparkline is visibly shorter than the rows above it
Where:
src/ui/gpu_sparkline_panel.rs:331-340(draw_sparkline_row).Problem:
draw_sparkline_rowcomputessparkline_width = content_width − fixed − badge_len. Because only the GPU Temp row has a badge, its sparkline isbadge_lencolumns shorter than the GPU Util and GPU Mem rows above it. The total row width is stillpanel_width(the missing columns are absorbed intopad), but the visible sparkline column looks ragged — the right end of the GPU Temp sparkline does not line up with the sparklines above it.Fix: This is fully resolved by fix #3. Once the thermal badge is removed,
badge_lenis 0 for every row andsparkline_widthbecomes identical across rows.A belt-and-braces improvement (optional, can be a follow-up): make
draw_sparkline_rowcomputesparkline_widthonce from the maximum badge_len acrossrowsand reuse that width for every row. That future-proofs the layout for any new per-row badge without re-introducing the mismatch.5.
@ <hostname>is redundant on GPU / CPU / Host Memory / Disk rows in local modeWhere:
src/ui/renderers/gpu_renderer.rs(print_gpu_info, ~line 133)src/ui/renderers/cpu_renderer.rs(print_cpu_info, ~line 263)src/ui/renderers/memory_renderer.rs(print_memory_info, ~line 75)src/ui/renderers/storage_renderer.rs(print_storage_info, ~line 95)src/view/frame_renderer.rs(render_local_devices,render_remote_devices,render_gpu_section)Problem: In local mode every device row shows
… @ Cube.lo …, but there's only one node — the host name is already surfaced in the top local-header bar (Host Cube.local · Apple M1 Ultra …) and in theNODEchassis row. Repeating it four more times on every frame is noisy and wastes ~12 columns per row that could otherwise be used for a longer device name or just left empty to lighten the visual.Fix: Add a
show_hostname: boolparameter to each of the fourprint_*_infodevice renderers (NOT toprint_chassis_info— the user explicitly kept the chassis NODE line in scope of the "@ hostname" hiding request, and the NODE line already uses a different formatNODE <host> Pwr:…without@; leave it alone to keep this issue tightly scoped). Whenshow_hostnameisfalse, skip the@ hostname_displaysegment entirely — do not emit even a blank equivalent, so the rest of the line (Util:,VRAM:, etc.) shifts left to fill the gap.Also skip looking up
hostname_scroll_offset/ invokingformat_hostname_with_scrollon the hidden-hostname path, and skip advancing the hostname scroll animation in local mode fromsrc/view/event_handler.rs(search forhost_id_scroll_offsets) — no user-visible scrolling hostname means no reason to burn CPU incrementing the offset.In
src/view/frame_renderer.rs, passshow_hostname: !view_state.is_local_modefrom therender_local_devices/render_gpu_section/render_chassis_sectioncall sites. The remote-mode callers (render_remote_devices) keepshow_hostname: true.Update the per-module tests to cover both states (
show_hostname = trueretains the@ hostnamesubstring in the stripped-ANSI output;show_hostname = falsedoes not).6. GPU Metrics sparkline panel doesn't show ANE even on Apple Silicon; add room for NPU on Intel
Where:
src/ui/gpu_sparkline_panel.rs:71-79(gpu_content_rows) and:215-300(build_rows/has_ane_data).Problem:
has_ane_datacurrently returnsfalsewhengpu.ane_utilization == 0.0, so the ANE row is hidden whenever the Neural Engine is idle — which is most of the time on a desktop M1 Ultra, even though the hardware clearly has an ANE. The user wants the ANE row to be always present on Apple Silicon so the panel reflects the real hardware layout. (The individual GPU info row inprint_gpu_infoalready draws an always-on ANE gauge atgpu_renderer.rs:311-330; this fix brings the top GPU Metrics panel into the same behaviour.)The user also asked to leave room for a similar "NPU" row on Intel CPUs once NPU telemetry lands (Meteor Lake / Core Ultra). This doesn't require real data today — just the scaffolding so that adding an Intel NPU reader later is a one-line UI change.
Fix:
has_ane_datatoshow_ane_row, and let it returndetect_apple_silicon(state)alone (drop theane_utilization > 0.0gate). Add a doc comment explaining that an ANE at 0 W is a meaningful "idle" reading and the row is load-bearing for platform identity even when idle.gpu_content_rowsto include the ANE row whenevershow_ane_rowis true — the row count becomes 5 on Apple Silicon regardless of current ANE power.show_npu_row(state: &AppState) -> boolhelper right next toshow_ane_row. For now it returnsfalse(no Intel/Windows NPU reader exists yet), and a short doc comment points atsrc/api/metrics/npu/common.rsas the hand-off point for future NPU data. Inbuild_rows, add a mirror of the ANE block that emits an "NPU" row (Yellow, same layout) whenshow_npu_rowis true, so that enabling it later is literally flipping the helper return value.test_gpu_content_rows_apple_with_aneexpectation from 5→5 (unchanged) and addtest_gpu_content_rows_apple_without_ane_data_still_shows_rowcoveringane_utilization == 0.0.As a follow-up (out of scope here), a dedicated
ane_power_history: VecDeque<f64>onAppStatewould give the ANE sparkline real history instead of the current single-pointvec![ane_w]. Worth mentioning in the issue body so the reviewer knows the sparkline trivialness is intentional, not forgotten.7. Make the
Processes:header look like the rest of the UIWhere:
src/ui/process_renderer.rs:59.Problem: Every other panel in the TUI uses either a full box (
CPU Cores,GPU Metrics) or a colored horizontal rule. The process section opens with a bare ASCII literalProcesses:followed directly by an un-labelled column header — it's the one unstyled artefact in the view and stands out as half-finished.Fix: Replace the single
queue!(stdout, Print("Processes:\r\n"))call with a one-line styled title rule that matches the rest of the UI, for example:Keep the line count unchanged (still exactly 1 row) so that
RESERVED_HEADER_ROWS = 4and the existingavailable_rows_for_processesmath continues to work without adjustment. An optional enhancement: append a compact[N total · M active]counter at the end of the title rule, formatted inDarkGrey, to make the header carry actual information instead of just looking pretty — but that's optional and can be split out.Acceptance Criteria
@ <hostname>no longer appears on GPU / CPU / Host Memory / Disk rows in local mode; it continues to appear in remote mode.ANErow on Apple Silicon regardless of current ANE power (including when power = 0 W).show_npu_rowhelper exists, returnsfalse, and is invoked frombuild_rowsso that flipping it totruelater yields an NPU row with no additional code changes.Processes:.cargo fmt --check,cargo clippy, andcargo testall pass.Non-goals
NODEchassis line (keep chassis hostname display as-is).ane_power_historyonAppState— the ANE sparkline stays single-point for this issue.show_hostname: boolparameter plumbing.