feat(ui): multi-line VM card + network-mode cycle restored#15
Merged
Conversation
Two user-reported asks land together here because they're tightly
coupled (the cycle action needs space to live on the card):
1. VM rows are now multi-line cards
The previous column-based table (Name | Status | OS | CPU | RAM |
Disk | LastUsed | Traffic) felt cramped and broke the mockup's
intended density when window widths shrank. New 62pt-tall card:
Row 1: <name> <status pill>
Row 2: <os> · <cores · GB ram> · <disk size>
Row 3: [⇄ network chip ▾] ↓/↑ rate pkts ▁▄▇
2. Network-mode cycle (lost when the right panel went away)
The mockup's right-side panel had a click-to-cycle network mode
target. That capability disappeared when the panel was removed
in the redesign. Restored here as the row-3 network chip — click
it to cycle:
NAT → Virtual (isolated) → Router → NAT
Cycling a running VM is blocked (Apple's Virtualization
framework attaches the network device once at VM-start; it
can't hot-swap). Stop-then-cycle is the correct workflow and
the click handler surfaces the standard alert with that
explanation when the user tries it on a live VM.
Implementation
==============
- `VMCardCellView`: NSTableCellView subclass with the three-row
layout. Right-edge subviews (status pill, sparkline, packets
label) anchor to bounds.maxX via layout() so they track on
table resize; left-edge labels reflow into the available width.
- AI Sandbox tab uses the same card with row 3 collapsed to a
static badge — TEMPLATE (orange) or SESSION (green) — instead
of a clickable network chip.
- `switchTableToCardMode(_:)` removes the XIB-defined columns at
runtime and replaces them with a single full-width VMCardColumn.
Idempotent — safe to call on every awakeFromNib.
- `VMManager.cycleNetworkMode(_:)` updates the in-memory list
+ persists to metadata.json. Pure step function
`nextNetworkConfig(after:)` is testable without singleton state.
- `VMError.networkModeChangeWhileRunning` carries the user-facing
explanation for why a running VM can't cycle (Apple framework
limitation, not our choice).
A guest VM (virtual-mode, routerVMId set) short-circuits straight
to NAT on the next cycle rather than getting dragged through the
canonical path with a stale routerVMId — keeps the cycle clean.
Tests
=====
New `VMCardCellAndCycleTests` — **14 passing**:
- Cycle: NAT → Virtual → Router → NAT loops correctly
- Guest VM cleanly drops to NAT, clearing the routerVMId
- Network chip text reflects each mode (NAT / ROUTER / GUEST /
ISOLATED)
- Status pill text matches each VMStatus
- Packet count formatter uses comma grouping (0 / 42 / 1,234 /
1,234,567)
- Bps formatter crosses B → kB → MB → GB thresholds
- Cell survives configure() with every status without crashing
Full suite: 264/264 green (one pre-existing flaky network test
skipped as before).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Resolves 8 conflicts in SecVF.xcodeproj/project.pbxproj — all of them "both branches added a file ref at the same insertion point" cases. Kept both sides' additions so VMCardCellView.swift, VMCardCellAndCycleTests.swift stay registered alongside the new files brought in by main: ISOCacheManagerWindow.swift, SwitchStatisticsWindowController.swift, TacticalSidebarSection.swift, SidebarFilterTests.swift. Also wired the deferred TODO: handleCycleNetworkMode now calls refreshSidebarCounts() so the Network section's badges (Isolated / Virtual / NAT) update immediately after a cycle, instead of staying stale until the next status-change notification. Full suite: 292/292 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 tasks
DaxxSec
added a commit
that referenced
this pull request
May 14, 2026
* fix(ui): three layout regressions from the typography pass merge Three visible regressions that the user reported after #15+#18 landed: 1. Sidebar section titles truncating ("ERATING SYSTEM", "TWORK", "GS") ================================================================== `TacticalSidebarSection.rebuild()` runs inside `init` before the caller has set the section's frame. It read `bounds.width` (=0 at that moment) and gave the title label / row views `width: bounds.width - inset * 2` = **-24pt**. The autoresizing mask (`.width`) compensates when the parent's bounds change, but it adds delta only — a section that grows from `bounds.width = 0` to `220` produces a title width of `-24 + 220 = 196`. That should be visible end-to-end, but NSTextField that's first sized with a negative-width frame caches its rendering metrics and the title gets visually left-truncated by ~inset pixels on every paint after. Fix: defer layout. `rebuild` now adds the subviews without sizing them; a new `relayoutContents()` runs from both `rebuild` (so the initial paint has a valid frame once the caller assigns one) AND from `layout()` (Cocoa hook fired whenever the section's bounds resolve). Layout constants moved to type-level statics so the two call sites share them. 2. VM table rows squished to ~20pt — multi-line card not showing ================================================================ The XIB hardcodes `tableView rowHeight="20"`. `switchTableToCardMode` sets `tableView.rowHeight = 62`, which SHOULD be authoritative — but when the XIB latches the lower value before the awakeFromNib call that runs the swap, NSTableView's first layout pass uses the latched 20pt and the card cell renders as a single squished line with row-3 chips spilling into row-1's name area. Fix: implement `tableView(_:heightOfRow:)` returning `VMCardCellView.rowHeight`. The delegate is queried on every row draw, so the value can't be stale or pre-empted by the XIB. 3. Detail-card status pill drifts off-center inside its 80pt bounds ==================================================================== The pill is an NSTextField with `alignment = .center`, `isBordered = false`, layer-backed background. On some macOS revisions the plain `alignment` property doesn't fully center text inside a borderless layer-backed label — the text drifts leftward, producing the dead-space halo on the right that appears as "the pill is spaced wrong". Fix: use `attributedStringValue` with an explicit centered NSMutableParagraphStyle. The paragraph-style centering bypasses NSTextField's plain alignment quirk and works deterministically. No behavior changes — these are all visual/positioning fixes. Full test suite still 292/292 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(ui): the VM-card row pill (the one user actually meant) User clarified the "pill is spaced wrong" report — they meant the status pill inside the VM-card row (e.g. "● RUNNING" / "○ STOPPED" on the right edge of row 1), not the detail card pill at the bottom of the window. My previous commit on this PR fixed the wrong pill. Same root cause + fix: NSTextField's plain `alignment = .center` drifts leftward on layer-backed borderless labels. Apply explicit centered NSMutableParagraphStyle via attributedStringValue on the VMCardCellView statusPill (running-state path) and the AI Sandbox TEMPLATE/SESSION pill (sandbox-bundle path). The previous fix to the detail card's makeStatusPill stays — same class of bug, two separate widgets, both needed the treatment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: DaxxSec <dax@example.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
User feedback after the merged redesign called out two things this PR addresses together:
Bundled because the cycle action needs a place to live — the new row-3 of the card.
The new card
62pt-tall row, three lines:
```
┌──────────────────────────────────────────────────────────┐
│ kali-router ● RUNNING │
│ Kali Linux 2025.1 · 4 cores · 4.0 GB · 32 GB disk │
│ [⬢ ROUTER] ↓ 12 kB/s · ↑ 0.3 kB/s 1,284 pkts ▁▄▇ │
└──────────────────────────────────────────────────────────┘
```
The row-3 chip is the network-mode target — clicking cycles:
```
NAT → Virtual (isolated) → Router → NAT
```
A guest VM (virtual-mode with a routerVMId pointing somewhere) cleanly drops to NAT in one step rather than dragging its stale guest relationship through the cycle.
Cycling a running VM is blocked — Apple's Virtualization framework attaches the network device once at VM-start and can't hot-swap it. The click still fires; the user gets an alert with that explanation.
AI Sandbox bundles use the same card with row 3 collapsed to a static badge: ◆ TEMPLATE (orange) for the base, ● SESSION (green) for children.
Implementation
VMCardCellView(new) —NSTableCellViewsubclass with the three-row layout. Right-edge subviews (status pill, sparkline, packets) anchor vialayout()so they track on table resize; left-edge labels reflow into available width via autoresize.switchTableToCardMode(_:)— drops the XIB-defined per-field columns at runtime and replaces them with a single full-width `VMCardColumn`. Idempotent.VMManager.cycleNetworkMode(_:)— updates the in-memory VM list and persists to metadata.json. Pure step function `nextNetworkConfig(after:)` is testable without touching the singleton.VMError.networkModeChangeWhileRunning— carries the user-facing alert text.Tests
New `VMCardCellAndCycleTests` — 14 passing:
Full suite: 264/264 green (one pre-existing flaky network test skipped as before).
Test plan
rect(ofRow:)so it should follow)Coordinating with other open PRs
#13 (detail-card layout fixes) and #14 (sidebar filter nav) both live off main. This branch is independent of both — its only mention of
refreshSidebarCountsis a code comment noting that when this and #14 merge in either order, the cycle handler should also call that to refresh the Network section's count badges.🤖 Generated with Claude Code