Commit 87ee7ac
feat(ui): multi-line VM card + network-mode cycle restored (#15)
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: DaxxSec <dax@example.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent a9d2bbb commit 87ee7ac
5 files changed
Lines changed: 715 additions & 14 deletions
File tree
- SecVF.xcodeproj
- SecVF
- Tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
33 | 33 | | |
34 | 34 | | |
35 | 35 | | |
| 36 | + | |
36 | 37 | | |
37 | 38 | | |
38 | 39 | | |
| |||
73 | 74 | | |
74 | 75 | | |
75 | 76 | | |
| 77 | + | |
76 | 78 | | |
77 | 79 | | |
78 | 80 | | |
| |||
129 | 131 | | |
130 | 132 | | |
131 | 133 | | |
| 134 | + | |
132 | 135 | | |
133 | 136 | | |
134 | 137 | | |
| |||
169 | 172 | | |
170 | 173 | | |
171 | 174 | | |
| 175 | + | |
172 | 176 | | |
173 | 177 | | |
174 | 178 | | |
| |||
250 | 254 | | |
251 | 255 | | |
252 | 256 | | |
| 257 | + | |
253 | 258 | | |
254 | 259 | | |
255 | 260 | | |
| |||
307 | 312 | | |
308 | 313 | | |
309 | 314 | | |
| 315 | + | |
310 | 316 | | |
311 | 317 | | |
312 | 318 | | |
| |||
475 | 481 | | |
476 | 482 | | |
477 | 483 | | |
| 484 | + | |
478 | 485 | | |
479 | 486 | | |
480 | 487 | | |
| |||
511 | 518 | | |
512 | 519 | | |
513 | 520 | | |
| 521 | + | |
514 | 522 | | |
515 | 523 | | |
516 | 524 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
0 commit comments