Skip to content

fix(796): keep the QuickFiler folder drop-down open and commit row selection before cancel - #807

Merged
drmoisan merged 15 commits into
mainfrom
bug/quickfiler-folder-dropdown-closes-on-open-796
Sep 7, 2026
Merged

fix(796): keep the QuickFiler folder drop-down open and commit row selection before cancel#807
drmoisan merged 15 commits into
mainfrom
bug/quickfiler-folder-dropdown-closes-on-open-796

Conversation

@drmoisan

@drmoisan drmoisan commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • Fixes the QuickFiler folder drop-down closing immediately on open, whether opened by clicking the arrow or by pressing Down in the search box.
  • Root cause, read from instrumentation rather than inferred: focusing the popup's WebView2 deactivates the QuickFiler form, and the deactivate handler's cancel loop runs before the open can finish. A self-inflicted-deactivation seam now distinguishes that case from a genuine deactivation.
  • Adds a pending-commit latch so a close arriving while a selection commit is in flight does not cancel the commit.
  • Extends the issue 680 leave-handoff latch to cover the mouse open path, not only the Down-arrow path.
  • Five of six acceptance criteria are delivered and verified. AC3 is partially delivered and is deliberately left unchecked; see Verification.
  • Feature review returned PASS with zero blocking findings.

Why

In the QuickFiler item view the folder drop-down flashed open and closed on every gesture. Typing did keep the list open, but clicking a row closed it without selecting; only Up and Down changed the selection.

The drop-down is not a ComboBox. It is a WebView2 breadcrumb page hosted by ItemViewer, plus a ToolStripDropDown popup hosting a second WebView2. That structure is what makes the defect possible: a ToolStripDropDown is not a Form, so giving focus to the popup's WebView2 deactivates the owning QuickFiler form, and the existing deactivate handler treats every deactivation as a reason to cancel the pending selector session.

The specification imposed an ordering constraint: the first implementation step had to be instrumentation, not a fix, so that the choice among three candidate close paths was made from evidence rather than from an assumption about Win32 activation ordering. That constraint was honoured. The instrumentation landed first, the reproduction was run against it, and the ordering was read off the log. Candidate 1 was confirmed and candidates 2 and 3 refuted on all three gestures.

What Changed

Core behaviour

  • QuickFiler/Controllers/QfcFormController.Deactivate.cs — gates the cancel loop on a self-inflicted-deactivation seam, so a deactivation caused by this add-in's own popup no longer cancels the selector session. A deactivation caused by any other window still does, preserving the issue 677 contract.
  • QuickFiler/Viewers/BreadcrumbDropDownHost.csFinishClose consults a pending-commit latch, so an uncommitted-reason close does not cancel while a commit is in flight.
  • QuickFiler/Controllers/QfcItemController.EventHandlers.cs — the search-leave dismissal latch is now also set on the mouse open path.
  • QuickFiler/Viewers/QfcFormViewer.cs, QuickFiler/Interfaces/IQfcFormViewer.cs — the intent member the deactivate handler reads, implemented at the one site that reads non-injectable activation state.
  • QuickFiler/Viewers/BreadcrumbDropDownHost.Open.cs, QuickFiler/Viewers/ItemViewer.Breadcrumb.cs — open-path and wiring changes supporting the above.

Diagnostics

  • QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs — new partial part carrying the AC6 instrumentation. OnDropDownClosed was relocated into it rather than duplicated, because the host file sits close to the repository's 500-line ceiling and declares no logger.

Tests

  • New: QuickFiler.Test/Viewers/BreadcrumbDropDownCloseOrderingTests.cs, QuickFiler.Test/Controllers/QfcItemController.SearchLeaveLatchTests.cs.
  • Updated: QuickFiler.Test/Controllers/QfcFormControllerDeactivateTests.cs, QuickFiler.Test/Viewers/BreadcrumbPendingOpenCloseTests.cs, QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs.

One pre-existing test was deliberately re-pinned rather than weakened. TextBoxSearchLeave_WhileDropDownOpen_RoutesExactlyOneCloseIntent came from the issue 680 fix and asserted that a search-box leave dismisses an open drop-down regardless of which gesture opened it. That is precisely the state AC4 requires to stop being dismissed. Its method name and its Times.Once() assertion are retained and one Arrange line establishing a search-driven open was added; the case it no longer covers is covered by a new test asserting Times.Never(). The class still declares six test methods.

Architecture / How It Fits Together

The selector session spans three components. ItemViewer hosts the collapsed breadcrumb; BreadcrumbDropDownHost owns the popup and its open/close lifetime; QfcFormController owns form-level deactivation. The defect lived in the seam between the third and the second: deactivation is a form-level event, but whether it should cancel depends on popup-level state.

The fix adds that missing information flow. QfcFormViewer exposes whether the current deactivation was caused by this add-in's own popup; QfcFormController.Deactivate consults it before cancelling; and BreadcrumbDropDownHost separately tracks whether a commit is in flight so that close ordering cannot lose a selection. No public API changed.

Verification

Completed

All commands were run against the branch head in the item worktree.

  • Formatting: dotnet tool run csharpier check . — exit 0 over 1608 files, empty unformatted list.
  • Analyzers: msbuild TaskMaster.sln /t:Rebuild ... /p:EnableNETAnalyzers=true /p:EnforceCodeStyleInBuild=true — exit 0, 0 warnings, 0 errors, against a baseline of 0 and 0. Non-vacuity established by 36 Csc task invocations and 36 csc.exe tool invocations with both touched assemblies rebuilt after the recorded run start.
  • Nullable: msbuild TaskMaster.sln /t:Rebuild ... /p:TreatWarningsAsErrors=true — exit 0, 0 warnings, 0 errors.
  • Tests: QuickFiler.Test assembly, Total 1380, Passed 1380, Failed 0, against a baseline of Total 1370 with an empty failure set.
  • Coverage: repository line coverage 24.1387 percent before (14867/61590) and 24.1857 percent after (14925/61710), so no regression. Changed-code coverage 40/41 = 97.5610 percent, above the 90 percent floor for changed code.
  • File sizes: all 15 audited paths at or under the 500-line ceiling, maximum 496.
  • Scope boundary: 84 paths against the merge base, none under UtilitiesCS/, UtilitiesCS.Test/, .claude/, .codex/, .agents/, config/ or .github/, and TaskMaster.sln untouched.

AC6 required a runtime ordering observation that needs a live Outlook process, a real WebView2 surface, a real ToolStripDropDown and human gestures. Repository unit-test policy forbids external processes and shown windows, so no automated test can produce it. It was handled as an approved human-interaction exception with a runbook and a recorded observation artifact, following the precedent of issues 400 and 438. Every acceptance criterion also retains at least one automatable managed-seam assertion.

AC3 is partially delivered

AC3 has two clauses. The second, that the selection is committed before any auto-close cancel runs, is delivered and pinned by tests in both polarities.

The first, that a mouse click on a row selects that row, has no post-fix observation behind it. The row-activation path was deliberately left unchanged, because the evidence did not support changing it, and the decision record states expressly that this establishes only that the evidence does not support the page change and not that the change is unnecessary. The claim that closing the deactivation-cancel path also removes the click-without-select symptom is a causal argument, and it has not been confirmed against a build carrying the fix.

AC3 is therefore left unchecked in spec.md and issue.md. This is a verification gap rather than a known defect, and feature review classified it as non-blocking. Closing it requires either one post-fix observation of the click gesture or a maintainer's acceptance of the causal argument.

Recommended

  • Exercise the three gestures against a live Outlook: arrow click, Down in the search box, and a mouse click on a row in the expanded list. The third is the one that closes the AC3 gap.

Backward Compatibility / Migration Notes

No breaking changes. No public API was changed, no file was renamed or removed, and no dependency was added. The issue 677 contract that a genuine deactivation cancels the selector session is preserved and pinned by tests in both polarities. The issue 438 regression guard on row-set refreshes is retained.

Risks and Mitigations

  • The deactivation seam reads non-injectable activation state, so its correctness rests on the observed framework behaviour rather than on a unit test of the framework itself. Mitigated by keeping that read at a single site and by testing every consumer through the interface.
  • The AC2 guard is sited in a method with two callers. The second is the Cancel teardown path, where no deactivation is in progress, so the guard degenerates there to "is a popup open?" and the teardown's selector-cancel stage is skipped when one is. Review traced the mitigation chain and found responsibility is retained through the group-cleanup stage, but it moves later and onto a fire-and-forget path. Recorded as a follow-up rather than fixed here, because narrowing the guard's siting is a behavioural change outside this item's write set.
  • Rollback is a straight revert. The diagnostics part is independently revertable and log-only.

Review Guide

Suggested order:

  1. QuickFiler/Controllers/QfcFormController.Deactivate.cs — the AC2 seam, the substance of the fix.
  2. QuickFiler/Viewers/BreadcrumbDropDownHost.cs — the AC3 commit-before-cancel ordering.
  3. QuickFiler/Controllers/QfcItemController.EventHandlers.cs — the AC4 latch.
  4. QuickFiler.Test/Controllers/QfcItemController.SearchDismissalTests.cs — the deliberate re-pinning, worth reading against the Test Strategy section of spec.md.
  5. Remaining tests and the diagnostics part.

QuickFiler/Viewers/BreadcrumbDropDownHost.Diagnostics.cs is largely a relocation of OnDropDownClosed and reads as new code in the diff without being new behaviour.

The documentation footprint is large relative to the code footprint. The feature folder carries the plan, specification, research, runbook, decision record and the per-task evidence artifacts. None of it is product code.

Follow-ups

  • Narrow the AC2 guard so it applies only to the deactivation caller, for example by passing an explicit flag from the deactivate handler and not from the Cancel teardown stage.
  • Remove or consume SearchOwnsDropDownDismissal in QfcItemController.EventHandlers.cs. The underlying field is live and is the AC4 latch; only this exposing property has no reader.
  • Correct the comment in BreadcrumbDropDownHost.cs stating that only the focus step is gated, which the new guard placement has falsified.
  • Reset the commit-pending latch on close as well as on show, so a restore after a failed open cannot consume a latch left from a previous popup lifetime.
  • Close the AC3 verification gap with one post-fix observation of the mouse-click gesture.

GitHub Auto-close

drmoisan and others added 15 commits September 7, 2026 00:03
Preparation-mode orchestration for issue #796: the QuickFiler item-view
folder drop-down flashes open and closes immediately on both the arrow
click and the Down key, and a row click closes the list without
selecting the row.

Delivers the promoted record, issue.md, spec.md, the research artifact,
the human-exception runbook, the preflight delta artifacts, and a
preflight-cleared 10-phase, 83-task atomic plan. No source file is
touched; atomic execution is out of scope for this run.

AC6 is carried as a hard ordering constraint on the plan rather than as
a criterion satisfied somewhere in it. Phase 1 contains only the
instrumentation of the two named close sites, Phase 2 is a manual
observation gate, Phase 3 records which of the three candidate close
paths was confirmed as the first cause, and no behavioural-fix phase
precedes that decision. The issue marks the Win32 activation ordering as
inferred rather than confirmed, and the plan specifies no fix that
presupposes it.

Confirming the runtime ordering needs a live Outlook process and human
gestures, which the unit-test policy forbids. That is recorded as a
permitted human-interaction exception with a runbook, and every
acceptance criterion retains at least one automatable managed-seam
assertion so none depends on manual verification alone.

Three preflight rounds resolved 12 blocking defects and 10 observations
across three planner revision passes and one orchestrator scope
adjudication. The most consequential: the plan's line-count idiom
omitted blank lines and would have halted Phase 0 on a false diagnosis;
a proposed interface member would have broken a compiled hand-written
implementor outside the write set; and the per-item diagnostic would
have dereferenced a reference the existing code guards, turning a silent
no-op into a logged error and making Phase 1 behavioural.

The write set is sixteen concrete paths confined to QuickFiler and
QuickFiler.Test. QuickFiler/Resources/FolderBreadcrumb.html is shared
with a concurrently prepared sibling item and the contention is recorded
rather than avoided by dropping the file.

Refs #796

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MS8fb2wX1Sx1bG67LAr41e
…tations

origin/main advanced by two sibling merges of this parallel run after the
plan cleared preflight. Merging it in invalidated citations the plan relies
on, so the plan is reconciled against the post-merge tree before execution.

Base anchor. Four acceptance gates (P1-T14, P7-T4, P9-T7, P9-T10) hardcoded
the pre-merge base c431dc3. Anchored there they enumerate the 146 files the
sibling merges brought in and bill them to this item, so every scope-boundary
gate would fail on work this item did not do and the P9-T7 changed-code
coverage denominator would be computed over sibling-authored lines. All four
now anchor on the merge commit c7ae69f. The cut point is retained on the
Branch metadata line, labelled as history rather than as the anchor. The
anchor was tested rather than read: diffing at c7ae69f lists nothing,
diffing at c431dc3 lists roughly 150 entries.

Citations the merge moved. The merge inserted three compile entries, at
post-merge line 336 of the QuickFiler project file and lines 66 and 148 of
the test project file, so the induced shift is positional rather than
uniform. Six citations were corrected accordingly, and the rationale now
states the positional rule instead of a uniform one-line rule that would
mislead a later re-derivation by one line.

Counts the merge invalidated. A line-number sweep does not cover counts. The
partial-part count for QfcItemController was eleven and is now twelve, the
merge having added the breadcrumb-wiring part, which declares only
EnsureBreadcrumbPipeline and so collides with no member Phase 1 adds.

Pre-existing inaccuracies found while sweeping and corrected in passing: the
claim that every project declares EnsureNuGetPackageBuildImports (seventeen
of eighteen do); an unsupported count of stalling shell-icon test classes,
removed rather than replaced because the membership is not established; the
claim that the repository has no root build-property file (it has both
Directory.Build.props and Directory.Build.targets, neither setting a nullable
property, so the instruction not to pass /p:Nullable=enable is unaffected);
the claim that every non-implementor reference to IQfcFormViewer is a mock or
a comment (there are production consumers, though the single-implementor
conclusion holds); and the target framework, which is 4.8.1 rather than 4.8.

No task was added, removed, or renumbered. The write set remains sixteen
paths, phase headings and LF endings are unchanged, and the plan validator
passes with no warnings.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…g sites

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
P1-T15 commits the instrumentation, but its own evidence artifact records
the SHA of that commit and so cannot be contained by it. The task's command
sequence carries no staging span, so the artifact and the P1-T14 and P1-T15
checklist transitions were left staged. This commit lands them.

No source file is touched. Both paths lie inside the feature folder and
within the declared write set, and the Phase 1 source footprint is unchanged
at the eight permitted paths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… the plan after merging main

The maintainer executed the Phase 2 runbook against branch head ec674e0 and
reported the result. Candidate 1, the form-deactivation cancel in
QfcFormController.ParkFocusAndCancelSelectors, is confirmed as the first cause on
all three gestures; candidate 2 is refuted on all three; candidate 3 was not
instrumented and the observed ordering leaves no room for it. This discharges
human-interaction requirement HI-796-1 and replaces the INFERRED activation
ordering with a measurement, which is what AC6 requires.

origin/main advanced by one merge while this item was halted at that gate, so it
was merged in at d78ae7f. The 59 incoming files touch nothing under QuickFiler
or QuickFiler.Test, and the merged tree rebuilds at 0 warnings and 0 errors.

The plan's diff gates are re-anchored, deliberately non-uniformly: P7-T4 and
P9-T10 move to d78ae7f because at the old anchor they would enumerate the
merge-borne files their own acceptance forbids, while P9-T7 is retained at
c7ae69f because it is scoped to a directory the merge did not touch and moving
it would drop the Phase 1 instrumentation out of the denominator it requires.
Line citations that the executed Phase 1 had moved are repaired and re-expressed
against member names rather than line numbers. Phase 4 gains a task repairing a
doc comment Phase 1 stranded.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e Phase 3 decision record

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… seam (AC2)

Declares IsDeactivationSelfInflictedByOwnPopup on IQfcFormViewer with false meaning GENUINE, implements it in QfcFormViewer from explicitly registered popup state rather than Form.ActiveForm (which the manual observation measured running inverted), wires the registration beside the may-take-focus assignment in ItemViewer.Breadcrumb.cs, and gates the per-item cancel loop on it. Focus parking stays unconditional per the AC2-PARK-FOCUS-SUPPRESSED: NO decision. Also repairs the stranded ParkFocusAndCancelSelectors doc comment and the refuted ActiveFormNull sentence introduced in Phase 1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… flight (AC3)

Adds an internal IsCommitPending latch on BreadcrumbDropDownHost.Open.cs, cleared as each fresh native show begins and set when an explicit-commit close reaches the host. FinishClose now cancels only when the reason is Uncommitted and no commit is in flight, per the AC3-ENFORCEMENT-SITE: HOST decision. The open coordinator and FolderBreadcrumb.html are untouched, the latter per AC3-HTML-POINTERDOWN: NOT REQUIRED.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…it opened (AC4)

Adds the SearchOwnedDismissalLatch to QfcItemController.EventHandlers.cs: a lifetime provenance flag set by the two search-driven open sites and consulted by TextBoxSearch_Leave, which now dismisses only a popup the search box opened. A mouse-driven open never sets it, so the mouse path needs no edit. The one-shot issue #680 handoff latch keeps its own lifetime and meaning unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ion guard

GestureOpen_ResolvesOpenAndLeavesHostOpenWithoutClose asserts at the managed seam that the gesture open resolves, the host stays open, the session reports the selector open and no Close reaches the host. RowSetRefreshWhileOpen_NeverClosesHost pins the issue #438 AC-3 contract across two row-set replacements while open, observing the session-preserving replacement path this item deliberately leaves out of its diff. Records the P7-T4 AC5 exclusion gate and a blocking finding: one pre-existing test outside the write set now pins the behaviour AC4 removes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ase 9 QA gates

QfcItemController.SearchDismissalTests.cs held a pre-existing issue #680 test asserting that a search-box leave dismisses any open drop-down. AC4 deliberately narrows that to popups the search box itself opened, so the test is re-pinned to the new contract rather than left asserting behaviour this item removed. The file entered the write set at that point, making it the seventeenth path.

Records the Phase 9 QA loop: csharpier format and check clean, the analyzer and nullable rebuilds at 0 warnings and 0 errors against a 0/0 baseline with 36 Csc invocations each, 1380 of 1380 QuickFiler.Test tests passing against a 1370 baseline with all four expect-fail inventory tests now green, and the coverage delta at 97.5610 percent changed-code coverage with the document ratio rising from 24.1387 to 24.1857 percent. Every write-set .cs and .html file is at or under the 500-line ceiling, the largest being 496.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Feature review returned zero blocking findings. It evaluated AC3 as
PARTIAL: the commit-before-cancel clause is delivered and pinned in both
polarities, but the clause asserting that a mouse click on a row selects
that row has no post-fix observation behind it. The row-activation path
was deliberately left unchanged, and the close-ordering decision record
states expressly that it establishes only that the evidence does not
support the page change, not that the change is unnecessary, and that the
question reopens if a row click still fails to select.

AC3 is therefore unchecked in spec.md and issue.md. The criterion text is
unchanged. This is a verification gap rather than a code defect, and the
reviewer recorded it as non-blocking.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@drmoisan
drmoisan merged commit 04a54e6 into main Sep 7, 2026
5 checks passed
drmoisan added a commit that referenced this pull request Sep 7, 2026
… issue #808

Files the non-blocking findings from PR #807's code review as one tracked
bug in the same three files: the AC2 guard also gating the #791 Cancel
teardown (CR-3), the commit-pending latch never cleared on consumption
(CR-2), a stale FinishClose comment (CR-1), a dead accessor (CR-4), and
the untested AC2 producer (CR-7).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: quickfiler-folder-dropdown-closes-on-open-and-click-does-not-select

1 participant