Fix ctrl+g/ctrl+b excursion re-arm to track needs-input identity, not count (BUG-069) - #909
Conversation
… count (BUG-069) The re-arm path (excursion==nil after a restore, needs-input count still >=1) froze a fresh snapshot on the very next rebuild regardless of whether anything new had actually happened. A stale, never-resolved needs-input role (e.g. an unrelated stuck (?)) kept the count >=1 indefinitely, so every ctrl+b re-armed and froze immediately, silently discarding any fold/selection change the operator made afterward until the next explicit restore — confirmed against two back-to-back live dogfood repros where ctrl+b replayed a stale, much earlier layout instead of the operator's actual prior position. Rail.armedNeedsInputIDs (Model.needsInputRoleIDs, hasNewNeedsInputID) now tracks the needs-input set by role identity: while no excursion is held, the baseline keeps refreshing to the current set on every rebuild for as long as it stays a subset of the last capture, and only freezes the instant a genuinely new, distinct role id appears — so the eventual freeze reflects the operator's latest position, not a stale one. The baseline updates unconditionally even while frozen, so a role that merely folds into an already-open excursion doesn't look "new" again after a later discharge. Separately confirmed (not fixed, narrow + pre-existing): currentRef() has no identity encoding for a cursor resting on an Archive-expando or Freelance-header fold row, so restoreCursor silently no-ops in that specific case — shared with the BUG-002 persisted-selection mechanism, out of scope here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
noteExcursionTransition's fresh-interruption arm fired unconditionally on a Rail's first-ever SetModel call, even when rows didn't exist yet (right after a TUI launch/relaunch). If a stale needs-input role already predated the launch, this froze a bogus snapshot: selRef=0 (no rows to resolve a cursor against) and fold state from whatever the PREVIOUS session had persisted to disk, not this session's navigation. Live repro: ctrl+b left both panes empty and flipped fold state to a stale prior-session layout instead of the operator's actual position. Fix: skip arming entirely (seed the baseline only) on the literal first-ever rebuild. Normal arm/re-arm rules apply from the second rebuild onward; ctrl+g's own EnsureExcursionArmed still arms from the operator's live position if nothing new interrupts them first.
Merging this branch changes the coverage (1 decrease, 1 increase)
Coverage by fileChanged files (no unit tests)
Please note that the "Total", "Covered", and "Missed" counts above refer to code statements instead of lines of code. The value in brackets refers to the test coverage of that file in the old version of the code. Changed unit test files
|
|
Update: found and fixed a second, related bug (BUG-070) while dogfood-testing the BUG-069 fix above. Live repro: relaunching the TUI while a stale, never-resolved needs-input role already existed (predating the launch) caused Root cause: Fix: Added regression tests ( |
Summary
excursion == nilafter a restore, needs-input count still >=1) froze a fresh snapshot on the very next rebuild regardless of whether anything genuinely new had happened. A stale, never-resolved needs-input role (e.g. an unrelated stuck(?)) kept the count >=1 indefinitely, so everyctrl+bre-armed and froze immediately — silently discarding any fold/selection change the operator made afterward until the next explicit restore.ctrl+breplayed a stale, much-earlier layout instead of the operator's actual prior position, and in the second (clean, controlled) test replayed the FIRST test's world instead of the second test's own manual navigation.Rail.armedNeedsInputIDs(Model.needsInputRoleIDs,hasNewNeedsInputID) tracks the needs-input set by role identity, not count. While no excursion is held, the baseline keeps refreshing to the current set on every rebuild for as long as it stays a subset of the last capture, and only freezes the instant a genuinely new, distinct role id appears — so the eventual freeze reflects the operator's latest position, never a stale one. The baseline updates unconditionally even while an excursion is frozen, so a role that merely folds into an already-open excursion doesn't look "new" again after a later discharge (would otherwise spuriously re-freeze).currentRef()has no identity encoding for a cursor resting on an Archive-expando or Freelance-header fold row, sorestoreCursorsilently no-ops in that specific case. Documented as a known follow-up candidate, not fixed here since the clean repro is fully explained by the identity-tracking bug alone.openspec/specs/hera-view/spec.md's excursion requirement/scenarios (same-feature bug fix, no new change folder per project convention) andcontext/knowledge/gotchas/hera-view.mdto correct the previously-wrong "intentional, not a bug" description of the count-edge re-arm.Test plan
internal/tui/hera/excursion_test.go:TestRail_ExcursionSnapshot_StaleItemDoesNotFreezeAwayManualNavigation(fails against pre-fix code, confirmed live),_DoesNotReArmAfterRestoreWithoutNewEntrant,_EntrantAbsorbedWhileFrozenDoesNotReFreezeAfterDischarge,TestModel_NeedsInputRoleIDs,TestRail_CurrentRef_ZeroOnArchiveExpandoRow(documents the separate confirmed-but-unfixed gap)go build ./...,go vet ./...,make fmt-check,make lint-pr(golangci-lint v2.11.4, 0 new issues)make test-cover-gate(race suite + 88% coverage floor) — green at 88.7%openspec validate hera-view --strict— validmake vulnreports 3 pre-existing stdlib CVEs (GO-2026-5856/5039/5037, unrelated to this change) — CI runs this step withcontinue-on-error: trueper.github/workflows/ci.yml🤖 Generated with Claude Code
Co-Authored-By: Claude noreply@anthropic.com