Repository navigation
fix(#410): Device Logs keeps the device you picked when another one connects or drops, and stops re-listing its SD card - #414
Conversation
…ected selection survives Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
/agentic_review |
PR Summary by QodoPreserve Device Logs selection across connection changes
AI Description
Diagram
High-Level Assessment
Files changed (3)
|
Code Review by Qodo🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)
Great, no issues found!Qodo reviewed your code and found no material issues that require reviewTip of the day💡 Did you know, you can describe a rule in plain language on the Rules page and Qodo drafts it for you |
|
/agentic_review |
|
Code review by qodo was updated up to the latest commit ed04818 |
|
7-way sweep —
|
|
Sweep: the combination now includes #415 — 8-way merge verified. Nothing merged, nothing pushed. The earlier sweep built #405 #406 #407 #408 #411 #413 #414 onto State re-derived
#415 on its own
8-way mergeTwo orders — octopus Full gate under one
Warnings improve, and nothing is addedBoth ends re-measured here with The one cross-PR hazard checked, and cleared#415 annotates production members nullable while #407/#408/#413 own the Test count reconciles1057 ( Not covered#363 (dependabot, Throwaway branches deleted. Nothing merged, nothing pushed. |
Sweep: #414 re-verified and built with the other five, onto today's
|
| check | result |
|---|---|
| ready-note SHA = head | ✅ note at 21:09:11Z names ed04818 = current head, no push since |
| CI green on that SHA | ✅ run 35468884235 "Build", head_sha ed04818, success |
| mergeable | ✅ MERGEABLE / CLEAN |
| qodo threads | ✅ 0 threads, 0 unresolved (author-leg control #196 returns 3, so the filter is live) |
| stale pipeline | ✅ newest .github/ change on main is 2026-09-08 |
| stale base |
⚠️ This PR's green predates all three merges
#405 merged 04:22:46Z, #406 04:25:27Z, #407 05:00:43Z (2026-09-20). This PR's newest Build run was created 2026-09-19T20:55:51Z — before all three. The badge is displayed against a main that no longer exists (strict: false), so the local build below is the only current evidence for this PR. Same is true of #408, #411, #413 and #415; only #416's run postdates the merges.
Specific to this PR — one false positive worth naming
A removed-member scan of this diff (the #189 heuristic: any removed public/abstract/virtual member needs a compile, not a status query) flags:
- public void RefreshSdCardFiles() { }
That is not a #189 shape. It is a no-op stub inside the RecordingStreamingDevice test double, replaced in the same diff by => Interlocked.Increment(ref _sdCardListings). The production member is untouched — AbstractStreamingDevice.cs:1662 and IStreamingDevice.cs:63 both still declare it, DeviceLogsViewModel.cs:483 still calls it, and DeviceLogsView.axaml:243 still binds RefreshSdCardFilesButton. Recording it because the heuristic cannot distinguish a production member from a test double's stub of the same name, and this one reads exactly like the dangerous case.
The internal DeviceLogsViewModel(IAppLogger?, ISdCardSessionImporter?) ctor change is .cs-only and therefore fully covered by the compile.
This PR also edits DeviceLogsViewModel.cs, inside the scope of the repo-wide BindingFacts source scanner that #405 tightened, and shares the shared-test-double surface with #408 (different files: Device/RecordingStreamingDevice.cs vs RecordingAppLogger.cs). Both cleared — see below. Test delta re-derived from the diff: +9.
The 6-way merge
origin/main d8ef0f9 ← #408 + #411 + #413 + #414 + #415 + #416, built in two orders
(octopus, and sequential 416→415→414→413→411→408). Both land on the same tree
ebe16170827e2f2c2cdb745a2ec41885a8921f40. One auto-merge, SerialPortPollingDeviceWatcherTests.cs.
git diff c15f693 ebe1617 — against the tree the 8-way sweep verified — is one file, +650/−0: BootloaderHoldServiceTests.cs. So #405/#406/#407 merged as squashes into exactly the content that sweep predicted, and #416 is textually orthogonal to the other five.
Full gate on ebe1617 — judged by exit code, not by grepping for Build succeeded
| leg | result |
|---|---|
desktop --no-incremental |
exit 0, 155 warning(s) |
iOS restore (no -r) |
exit 0 |
iOS Release -r iossimulator-arm64 --no-restore --no-incremental |
exit 0, 156 warning(s) |
| HeadlessBench restore | exit 0 |
HeadlessBench Release --no-incremental |
exit 0, 156 warning(s) |
dotnet test |
1116 passed / 0 failed / 0 skipped |
Working tree clean, no packages.lock.json drift. The HeadlessBench leg demonstrably compiled — its output carries Program.cs(2060,13): warning CS0618, not a stale-assembly no-op.
Test reconciliation — each delta re-derived from that PR's own diff
main d8ef0f9 = 1057 (at 858d143) + 19 (#405/#406/#407 together) = 1076.
| #408 | #411 | #413 | #414 | #415 | #416 |
|---|---|---|---|---|---|
| +0 | +3 | +8 | +9 | +0 | +20 |
1076 + 0 + 3 + 8 + 9 + 0 + 20 = 1116 ✅
Hazards checked by compilation, not by argument
- chore(warning-zero): CS8625 ×6 — the members that are documented as null-when-empty are now declared nullable (8 lines, 4 files) #415's nullable annotations × the test-side PRs.
Daqifi.Avalonia.Tests.csprojhas<Nullable>enable</Nullable>+<TreatWarningsAsErrors>true</TreatWarningsAsErrors>, so anyCS8602/CS8604/CS8767cascade into test: the last two private IAppLogger fakes fold into RecordingAppLogger — one logger double in the suite (−44) #408's folded logger, test(#409): the flash probe's cancellation token and the no-document warning are now pinned through app code, not only Core #411's harnesses, fix(#412): one NaN reading no longer fails an SD import — dropped and reported, as the live path does (#294) #413's/fix(#410): Device Logs keeps the device you picked when another one connects or drops, and stops re-listing its SD card #414's/test(coverage-gap): pin BootloaderHoldService — the hold that decides whether a firmware flash can proceed #416's new tests would have failed the build. It compiled. Cleared. - test: the last two private IAppLogger fakes fold into RecordingAppLogger — one logger double in the suite (−44) #408 and fix(#410): Device Logs keeps the device you picked when another one connects or drops, and stops re-listing its SD card #414 both alter shared test doubles — different files, zero overlap (
RecordingAppLogger.csvsDevice/RecordingStreamingDevice.cs). The suite-wide consumer risk is what the compile + 1116 tests rule out. Cleared. - Duplicate top-level types (fix(notifications): the flyout names its empty state instead of rendering a blank panel #258 shape). test(coverage-gap): pin BootloaderHoldService — the hold that decides whether a firmware flash can proceed #416's new file declares exactly one top-level type;
FakeHidTransport/ConnectCallare nested. No name introduced by any of the six exists onmainor in another PR. Cleared. - test(#388): the dialog view-model guard requires the initializer to END at the construction — new VM(...).ToString() no longer passes #405's repo-wide guard.
BindingFacts.cs:270scansDaqifi.Avalonia/**/*.cs— production, so fix(#410): Device Logs keeps the device you picked when another one connects or drops, and stops re-listing its SD card #414's and chore(warning-zero): CS8625 ×6 — the members that are documented as null-when-empty are now declared nullable (8 lines, 4 files) #415's edits are in scope. Run explicitly on the merged tree:--filter "FullyQualifiedName~Binding"→ 175 passed, 0 failed, includingBindingFactsConstructionTestsand the dialog/DataContext guards. Cleared.
Not claimed
#363 (dependabot, UNKNOWN) was not built with any of these. Android not built here — CI covers it. No visual re-run and none needed: the merged diff is 15 files, all .cs; no .axaml and no baseline manifest. No bench run — none of the six is device-observable. main's own warning baseline was not re-measured, so no warning delta is claimed.
Full log: ~/.claude-daqifi/avalonia-loop/logs/20260920T053803Z-sweep-6way.md.
Nothing merged, nothing pushed.
Closes #410. Not merging — for review.
What was wrong
In Logged Data → DEVICE LOGS, with two or more devices connected, a connect or disconnect of any other device moved the DEVICE combo back to the first device. That device's SD card was listed in place of the one the user picked, and IMPORT ALL then targeted it. Nothing told the user the selection had moved. And with a single device selected, every other device connecting still sent that device a fresh SD listing.
The cause was
DeviceLogsViewModel.UpdateConnectedDevices. It rebuiltConnectedDeviceswithClear()followed by re-adding every device. When the combo's items emptied, its two-waySelectedItembinding wrotenullintoSelectedDevice. TheSelectedDevice == nullfallback then pickedFirst(), and each of those two writes ranOnSelectedDeviceChanged, which started an SD listing.The fix
UpdateConnectedDevicesnow updates the list in place instead of rebuilding it. It removes only the devices that left and appends only the ones that arrived. A selected device that is still connected never leaves the combo's items, so nothing writes to the selection and no SD listing is sent.When the selected device itself leaves, the pane falls back to the first device still connected and lists that device's card, as before. If no device is left, the selection is cleared. The view-model now makes that check itself (is the selection still in the list?) instead of only checking for
null, so it no longer depends on the combo to clear the selection first.Mobile shares this code.
DeviceLogsMobileView.axamlbinds the sameDeviceLogsViewModelwith the same two-waySelectedItem="{Binding SelectedDevice}"(it is created inStorageMobileView.OnDeviceLogs), so this one change fixes both views.Considered and left alone: equality. The update uses
List.Contains, which followsEquals, and the WiFiDaqifiStreamingDeviceoverridesEqualsto compare Name, IP and MAC. The pane's list and the registry could only disagree about which of two value-equal instances is present if the registry swapped one for the other without a notification in between. That cannot happen:RegisterConnectedDevicerefuses a value-equal duplicate, and every registry change raises"ConnectedDevices"synchronously, so a reconnect is always a removal update followed by an arrival update.ConnectionManageritself uses the same value equality.Also a test seam: the existing
internaltest constructor takes an optional UI-thread marshal (Action<Action>). This is the same patternDatabaseLoggerandConnectionDialogViewModelalready use. Outside a running app,Dispatcher.UIThreadbinds to the first thread that touches it and is never pumped, so the constructor's own update would hang any test running on another thread. Production passesnulland keeps theDispatcher.UIThreadpath.The UI is unchanged; this is a behaviour fix, so the tests are the evidence. The issue's screenshots show the "before".
Verification
DeviceLogsSelectionTestshas 9 new tests. They register real notifications throughConnectionManager.Instanceand useRecordingStreamingDevice, which now counts SD listings. This test project builds no Avalonia controls, so a small stand-in plays the combo: when the selected item is no longer in the items, it writesnullintoSelectedDevice. That is the behaviour the issue recorded against the real control ([null, AAAA0001]).7 of 9 fail against the old update logic. I ran them twice, with the old
Clear()+re-add body restored and the seam kept so the tests could run:AAAA0001AAAA0001(the test re-picks B after the connect, so it measures the drop alone)[null, "AAAA0001"], which is exactly the issue's traceBBBB0002AAAA0001(this only fails when no view is attached; with a combo, the old code also ended atnull)The 2 that pass on the old code are guards on behaviour that must not change: the fallback to the first remaining device with the combo attached (including its one SD listing), and the list following connects and drops in order.
iossimulator-arm64 --no-restore✅. Android is not built on this Mac; CI covers it.dotnet test: 1066 passed, 0 failed (locally).858d143run 35463500927 vs this headed04818run 35468884235): 148 distinct first-party warnings on both sides, 166 all-project on both. Ignoring line numbers, 0 were added and 0 removed. The only three entries that differ are the existingDeviceLogsViewModelconstructor CS8618s (_busyMessage,_selectedDevice,_deviceFiles), which moved down by the seam's lines.ed04818.RefreshSdCardFilescalls.Diffstat
Production code is
DeviceLogsViewModel.cs+44/−10; most of the added lines are comments, plus the seam. Tests:DeviceLogsSelectionTests.cs+324 (new) andRecordingStreamingDevice.cs+9/−1. The diff is net-additive because it is a bug fix with its regression tests.🤖 Generated with Claude Code