mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-11 23:40:28 -10:00
* fix(local): subchat mouse selection + hidden plan panel + scroll-extend selection Three bugs in the subagent/swarm drill-in (subchat) view and mouse text selection, all from the same root: newer features (subchat's row-source swap, wheel-scroll) were never integrated into the older selection/footer code. 1) Can't copy anything in the subchat view. transcriptLineAtMouse (and selectedTranscriptText, the copy-time text extraction) always hit-tested against m.transcript (the parent), never m.subchat.childRows, even while the screen was showing the child session. New transcriptHitTestSource() mirrors transcriptView's own branching (nav bar + child rows + full width vs pinned title bar + parent rows + sidebar-aware width) and is now the single source both functions use. 2) Plan panel shown above the composer inside the subchat view. footerView unconditionally rendered the PARENT run's pinned plan panel; only pendingAskUser was gated. Added a !m.subchat.active guard (m.plan belongs to the parent run, not whatever subagent/swarm session is being viewed). 3) Selection didn't extend across a scroll (reported as "stopped at the end of the screen"). transcriptSelection.cursor was only ever updated by a mouseMotion event; a wheel event is a distinct message type that never reached that code, so scrolling while selecting left the selection frozen at whatever line the mouse last physically moved over. New scrollChatExtendingSelection scrolls AND re-evaluates the cursor at the mouse's current position against the post-scroll viewport. Uses a new nearestTranscriptLineAtMouse (falls back to the closest visible selectable line) rather than an exact match, since the recomputed position can land exactly on a blank spacer row between messages after an odd-length scroll shifts text/spacer parity - an exact-match miss there must not silently no-op the extension. Tests: subchat selection resolves the CHILD session's text, not the parent's; the plan panel is absent from the footer during subchat; a selection's cursor bodyY moves (extends) after a wheel-scroll instead of staying pinned. Full gate green (73 pkg, -race). * feat(local): highlight clickable rows on mouse hover Moving the cursor over an interactive row now highlights it (accent underline) before you click, across every clickable surface: - specialist cards and collapse/expand toggle headers in the transcript - AGENTS sidebar rows (swarm members) - PLAN sidebar step rows - permission-prompt options (reuses the EXISTING keyboard-cursor highlight by moving pendingPermission.cursor on hover, instead of new styling) Requires switching the mouse-tracking mode from CellMotion (only reports motion while a button is held) to AllMotion (reports idle cursor movement too) - the existing 15ms mouse-event throttle already covers the added motion events. hover.go: hoverTarget + mouseHover predicate + updateHoverTarget, which reuses the SAME hit-testers and priority order as the existing click handlers so hover and click never disagree about what's clickable. Sidebar hover is resolved by STABLE IDENTITY (sessionID / stepIndex), not a cached line offset: hoveredSidebarLineOffset re-matches the identity against a freshly computed hit list on every render, so a row that's disappeared (a swarm member's linger elapsed, a plan step completed) simply doesn't highlight rather than a coincidentally-matching unrelated row lighting up. Transcript hover stays bodyY-keyed (matching the existing selection-highlight convention) with explicit clearHover() calls wherever bodyY numbering can shift without an intervening mouse motion: wheel-scroll, PgUp/PgDn, window resize, and transcript rebuilds (/clear, /resume, /rewind, /compact via resetFlushFrontier). Adversarial review caught two real bugs before ship, both fixed: - BLOCKER: plan-step hover stored the step's own index (0,1,2...) but the sidebar render treated it as a raw line offset into the rendered rows - every plan-step hover highlighted the wrong row (the AGENTS header/body). Fixed by resolving fresh at render time via the stable-identity redesign above, which also closes the related sidebar-drift issue. - MAJOR: hover wasn't cleared on keyboard PgUp/PgDn scroll. Tests: predicate correctness; hover resolves the right target across all 4 surfaces (specialist card, toggle row, sidebar agent, plan step, permission option) with the correct priority order; hover changes the actual render output (transcript + sidebar); hover clears on wheel-scroll and subchat exit; a regression test proving hoveredSidebarLineOffset self-heals (no highlight) when a hovered row's identity no longer resolves - the exact scenario the blocker fix targets. Full gate green (73 pkg, -race). * fix(local): soften hover color + auto-scroll a drag past the transcript edge 1) Hover highlight was brand-lime (bright, glaring against the near-black surface) and re-paints on every mouse movement. Switched to amber (an existing semantic token, "light orange") and dropped the underline per feedback. 2) Dragging a text selection past the top/bottom edge of the visible transcript now auto-scrolls toward that side and extends the selection to follow, matching the browser/editor "drag past the viewport edge" pattern. transcriptEdgeScrollDelta detects the drag is vertically outside the body rect (but still within its horizontal span, so it isn't just "off to the side" over the sidebar); dragToEdgeScroll scrolls one step and anchors the cursor to whichever line now sits at that edge of the NEW viewport - deliberately not a re-resolve of the drag's physical mouse position, which stays outside the body rect after scrolling (scrolling changes what content occupies a screen row, not the mouse's screen position) and would keep finding nothing. Extracted nearestTranscriptSelectableAt as the shared "closest selectable line to a target bodyY" core, now used by both nearestTranscriptLineAtMouse (target derived from a mouse position that may have landed on a spacer row) and dragToEdgeScroll (target derived directly from the viewport bound, no mouse position involved at all). Tests: dragging past the top edge scrolls toward older content and extends the cursor upward; the symmetric bottom-edge case scrolls toward newer content and extends downward. Full gate green (73 pkg, -race). * fix(local): smooth-glide the drag-to-edge auto-scroll instead of jumping The initial edge-scroll implementation (previous commit) applied a single chatWheelScrollLines (5-line) jump per raw mouse-motion event, which reads as sudden/jerky since motion events during a real drag arrive in bursts, and stops dead the instant the physical mouse holds still even while still parked past the edge (a terminal only reports motion on actual cursor movement). Replaced with a self-scheduling tea.Tick chain (dragEdgeScrollTickMsg, gated by edgeScrollSeq mirroring the existing exitConfirmSeq/copyStatusSeq idiom) that steps a small, fixed increment (dragEdgeScrollStep=1 line) on a short cadence (dragEdgeScrollInterval=70ms) for as long as the drag holds past the edge, independent of whether new raw motion events keep arriving. reducedMotion still gets the original single-bigger-step-per-event behavior (no animated glide). Adversarial review of the new self-perpetuating tick chain (the risk being an orphaned/stuck chain) found one real bug: a keypress mid-drag clears m.transcriptSelection.active directly (the KeyPressMsg handler in model.go) without going through stopEdgeScroll, leaving edgeScrollDelta stale. The in-flight tick itself terminates safely via its own liveness check - the bug was that the STALE nonzero edgeScrollDelta then fooled startEdgeScroll's "already running" fast path on the NEXT same-direction drag into silently doing nothing (no immediate step, no scheduled tick) for that whole hold. Fixed at the press site (transcript_selection.go's mouseLeftPress case): every fresh press now resets glide state via stopEdgeScroll, so a new drag always starts clean regardless of how the previous one ended - a single targeted fix rather than chasing every place transcriptSelection can be cleared. Tests: top/bottom edge glide starts and extends the cursor over several ticks (a single small step can land on a blank spacer row between messages, same as real usage); the chain stops when the drag returns to the body and a stale in-flight tick after that is a safe no-op; the keypress-interrupt regression (reproduces the adversarial review's finding, confirms the fix). Full gate green (73 pkg, -race). * fix(local): soften the brand-lime accent color across the whole UI The accent color (#caff3f) was fully saturated at fairly high lightness (HSL: L 62%, S 100%) - the kind of value that reads as neon/glaring everywhere it filled a block or bold-text surface, not just the one spot originally flagged: the composer's "typed" prompt glyph, the permission popup's focused-option badge, and the text-selection highlight all use it, plus the spinner, the brand chip, tool-card prompts, and more. Rather than patch each of those consumers individually, softened the single palette source (dark theme only; the light theme's accent is a separate, already-darkened value for its own contrast needs, untouched) to L 50% / S 80% (#ade619) - still clearly on-brand lime, and contrast against both black (onAccent text on accent fills) and the near-black panel stays far above WCAG AA (~14:1 and ~13:1, both comfortably over the 4.5:1 floor). Updated TestStreamingFadePaletteMonotonic's pinned RGB assertion (it checked the fade ramp's first bucket equals the literal old accent bytes) to the new values. Full gate green (73 pkg, -race; make lint/go vet clean except an unrelated stray file that isn't part of this branch or git history at all). * fix(local): darken the brand-lime accent further per live feedback Two prior passes (#ade619 at L50/S80, #8db620 at L42/S70) were each tested live against the original #caff3f via raw truecolor escape codes and confirmed too subtle in turn. Settled at L36/S62 (#759523) - a noticeably darker, more olive lime, confirmed against the original side by side. Contrast vs black (onAccent) and the near-black panel stays comfortably above WCAG AA (~6.1:1 and ~5.6:1). Updated the streaming-fade test's pinned RGB assertion to match. * fix: address PR #350 review comments and CI smoke test failures Smoke tests (macOS + Ubuntu): the 4 new edge-glide tests implicitly relied on m.reducedMotion being false, but mouseTestModel() inherits it from defaultReducedMotion(), which forces true whenever colorprofile.NoTTY is detected - true in CI's non-TTY test runners, false in a local interactive dev shell. Set reducedMotion = false explicitly in each of the 4 tests so they're deterministic regardless of environment. Verified by re-running locally under ZERO_REDUCED_MOTION=1 (reproduces the CI condition) - passes. CodeRabbit actionable comment: handleTranscriptSelectionMouse's mouseMotion case gated only on transcriptSelection.active, which stays true through the async copy-command grace window after release (a non-empty selection isn't reset until transcriptCopiedMsg lands) - so a genuine hover motion arriving in that window would be misrouted as a drag update. The suggested fix (gate on msg's own Button field) turned out to be wrong in a way only a full test run caught: TestTranscriptSelectionUpdatesOnGenericMotion is a pre-existing, deliberate test - some terminals don't restate the button on every motion event during a real drag, so trusting the per-event Button field breaks those terminals. Fixed properly instead: added a `dragging` field to transcriptSelectionState, tracking the app's own press/release bracket (true at press, false at release) independent of active and independent of any single event's Button field. mouseMotion now gates on dragging. CodeRabbit nitpicks: routed the two remaining raw `m.hover = hoverTarget{}` subchat-exit assignments through the existing clearHover() helper, so hover invalidation stays in one place. Left the third nitpick (hit-test layout recomputed per hover-motion tick) as a documented follow-up per the reviewer's own "trivial/poor tradeoff" framing - a proper per-frame memoized cache needs correct invalidation on scroll/resize/content-change to avoid reintroducing the exact staleness class of bug already fixed twice in this branch, and isn't worth rushing. New tests: TestTranscriptHoverAfterReleaseDoesNotMoveSelection (the actual bug CodeRabbit found - a post-release hover must not alter the just-copied selection). Full gate green (73 pkg, -race); TestTranscriptSelectionUpdatesOnGenericMotion (the test the first fix attempt broke) verified still passing.
285 lines
10 KiB
Go
285 lines
10 KiB
Go
package tui
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
tea "charm.land/bubbletea/v2"
|
|
|
|
"github.com/Gitlawb/zero/internal/agent"
|
|
)
|
|
|
|
func TestMouseHoverPredicate(t *testing.T) {
|
|
if !mouseHover(testMouseMotion(tea.MouseNone, 5, 5)) {
|
|
t.Error("a motion event with no button pressed must be a hover")
|
|
}
|
|
if mouseHover(testMouseMotion(tea.MouseLeft, 5, 5)) {
|
|
t.Error("a motion event WITH a button pressed is a drag, not a hover")
|
|
}
|
|
if mouseHover(testMouseClick(tea.MouseLeft, 5, 5)) {
|
|
t.Error("a click is not a hover")
|
|
}
|
|
if mouseHover(testMouseWheel(tea.MouseWheelDown, 5, 5)) {
|
|
t.Error("a wheel event is not a hover")
|
|
}
|
|
}
|
|
|
|
func TestUpdateHoverTargetOnSpecialistCard(t *testing.T) {
|
|
m := specialistClickTestModel(t, "child-sess-1")
|
|
x, y, ok := specialistCardClickPoint(m, 2)
|
|
if !ok {
|
|
t.Fatal("expected a specialist card selectable line in the layout")
|
|
}
|
|
|
|
updated, _ := m.Update(testMouseMotion(tea.MouseNone, x, y))
|
|
m = updated.(model)
|
|
if m.hover.kind != hoverTranscript {
|
|
t.Fatalf("hover.kind = %v, want hoverTranscript over a specialist card", m.hover.kind)
|
|
}
|
|
}
|
|
|
|
func TestUpdateHoverTargetOnToggleRow(t *testing.T) {
|
|
m := mouseTestModel()
|
|
m.transcript = appendTranscriptRow(m.transcript, transcriptRow{kind: rowReasoning, text: "private thought"})
|
|
|
|
width := m.chatColumnWidth()
|
|
body, selectable := m.transcriptBody(width, "")
|
|
start, _, top := m.transcriptViewportStart(body, width)
|
|
var target transcriptSelectableLine
|
|
for _, line := range selectable {
|
|
if line.toggle {
|
|
target = line
|
|
break
|
|
}
|
|
}
|
|
if !target.toggle {
|
|
t.Fatalf("expected reasoning header to be clickable, selectable=%#v", selectable)
|
|
}
|
|
|
|
updated, _ := m.Update(testMouseMotion(tea.MouseNone, target.textStart, top+target.bodyY-start))
|
|
m = updated.(model)
|
|
if m.hover.kind != hoverTranscript {
|
|
t.Fatalf("hover.kind = %v, want hoverTranscript over a toggle header", m.hover.kind)
|
|
}
|
|
if m.hover.bodyY != target.bodyY {
|
|
t.Fatalf("hover.bodyY = %d, want %d", m.hover.bodyY, target.bodyY)
|
|
}
|
|
}
|
|
|
|
func TestUpdateHoverTargetOnPlainTextIsNone(t *testing.T) {
|
|
m := mouseTestModel()
|
|
m.transcript = appendRow(m.transcript, rowUser, "hello world")
|
|
textY := firstTranscriptTextMouseY(t, m)
|
|
|
|
updated, _ := m.Update(testMouseMotion(tea.MouseNone, 3, textY))
|
|
m = updated.(model)
|
|
if m.hover.kind != hoverNone {
|
|
t.Fatalf("hover.kind = %v, want hoverNone over plain (non-clickable) text", m.hover.kind)
|
|
}
|
|
}
|
|
|
|
func TestUpdateHoverTargetOnSidebarAgentRow(t *testing.T) {
|
|
// Only a SWARM member row (a session mapped via swarmSessionMap) is clickable
|
|
// in the sidebar — a Task-delegation specialist row is not (sidebarAgentRows
|
|
// only records hits from the swarm loop). swarmSidebarTestModel builds exactly
|
|
// that: real conversation + a mapped swarm member session.
|
|
m := swarmSidebarTestModel(t, map[string]string{"subagent-1": "sess-1"})
|
|
if !m.sidebarActive() {
|
|
t.Fatal("sanity check failed: sidebar should be active with a swarm member present on a 100-col terminal")
|
|
}
|
|
hits := m.sidebarAgentSelectables(sidebarWidth(m.width))
|
|
if len(hits) == 0 {
|
|
t.Fatal("expected at least one clickable sidebar agent row")
|
|
}
|
|
x0 := m.chatColumnWidth() + 3
|
|
|
|
updated, _ := m.Update(testMouseMotion(tea.MouseNone, x0+1, hits[0].lineOffset))
|
|
m = updated.(model)
|
|
if m.hover.kind != hoverSidebarAgent {
|
|
t.Fatalf("hover.kind = %v, want hoverSidebarAgent", m.hover.kind)
|
|
}
|
|
if m.hover.sessionID != hits[0].sessionID {
|
|
t.Fatalf("hover.sessionID = %q, want %q", m.hover.sessionID, hits[0].sessionID)
|
|
}
|
|
// The bug this guards: hoverTarget must store the STABLE identity (sessionID),
|
|
// not a raw line offset — resolve it back to a line offset the same way the
|
|
// render path does (hoveredSidebarLineOffset) and confirm it lands on the
|
|
// SAME row that was actually hovered, not some other row at that raw index.
|
|
resolved, ok := m.hoveredSidebarLineOffset(sidebarWidth(m.width))
|
|
if !ok || resolved != hits[0].lineOffset {
|
|
t.Fatalf("hoveredSidebarLineOffset() = (%d, %v), want (%d, true)", resolved, ok, hits[0].lineOffset)
|
|
}
|
|
}
|
|
|
|
func TestUpdateHoverTargetOnPlanStep(t *testing.T) {
|
|
m := runningPlanModel(t, 3)
|
|
m.altScreen = true
|
|
m.height = 40
|
|
// sidebarActive requires real conversation; runningPlanModel only sets m.plan,
|
|
// leaving the transcript at its default (welcome-only, i.e. "empty").
|
|
m.transcript = appendRow(m.transcript, rowUser, "do something")
|
|
if !m.sidebarActive() {
|
|
t.Fatal("sanity check failed: sidebar should be active with a plan present on a 100-col terminal")
|
|
}
|
|
hits := m.sidebarPlanSelectables(sidebarWidth(m.width))
|
|
if len(hits) == 0 {
|
|
t.Fatal("expected at least one clickable plan step row")
|
|
}
|
|
x0 := m.chatColumnWidth() + 3
|
|
|
|
updated, _ := m.Update(testMouseMotion(tea.MouseNone, x0+1, hits[0].lineOffset))
|
|
m = updated.(model)
|
|
if m.hover.kind != hoverPlanStep {
|
|
t.Fatalf("hover.kind = %v, want hoverPlanStep", m.hover.kind)
|
|
}
|
|
if m.hover.stepIndex != hits[0].stepIndex {
|
|
t.Fatalf("hover.stepIndex = %d, want %d", m.hover.stepIndex, hits[0].stepIndex)
|
|
}
|
|
// The bug this guards: the render path must resolve stepIndex back to the
|
|
// CORRECT current lineOffset (hits[0].lineOffset, which is >= base=4, never a
|
|
// raw small index like 0/1/2) — not treat stepIndex itself as a line offset
|
|
// into the rendered sidebar (which would land on the AGENTS header/body).
|
|
resolved, ok := m.hoveredSidebarLineOffset(sidebarWidth(m.width))
|
|
if !ok || resolved != hits[0].lineOffset {
|
|
t.Fatalf("hoveredSidebarLineOffset() = (%d, %v), want (%d, true)", resolved, ok, hits[0].lineOffset)
|
|
}
|
|
}
|
|
|
|
func TestUpdateHoverTargetOnPermissionOptionMovesCursor(t *testing.T) {
|
|
m := mouseTestModel()
|
|
m.pending = true // the permission prompt only renders into the transcript body while m.pending
|
|
m.pendingPermission = &pendingPermissionPrompt{
|
|
request: agent.PermissionRequest{ToolName: "write_file"},
|
|
cursor: 0,
|
|
}
|
|
width := m.chatColumnWidth()
|
|
body, selectable := m.transcriptBody(width, "")
|
|
start, _, top := m.transcriptViewportStart(body, width)
|
|
var target transcriptSelectableLine
|
|
found := false
|
|
for _, line := range selectable {
|
|
if line.permOption && line.permChoice != permissionOptions(m.pendingPermission.request)[0].choice {
|
|
target = line
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
t.Fatal("expected at least 2 permission options to test a non-default hover target")
|
|
}
|
|
|
|
updated, _ := m.Update(testMouseMotion(tea.MouseNone, target.textStart, top+target.bodyY-start))
|
|
m = updated.(model)
|
|
// Permission-option hover reuses the existing cursor highlight, not m.hover.
|
|
if m.hover.kind != hoverNone {
|
|
t.Fatalf("hover.kind = %v, want hoverNone (permission options use pendingPermission.cursor)", m.hover.kind)
|
|
}
|
|
wantIndex := -1
|
|
for index, option := range permissionOptions(m.pendingPermission.request) {
|
|
if option.choice == target.permChoice {
|
|
wantIndex = index
|
|
break
|
|
}
|
|
}
|
|
if m.pendingPermission.cursor != wantIndex {
|
|
t.Fatalf("pendingPermission.cursor = %d, want %d (the hovered option)", m.pendingPermission.cursor, wantIndex)
|
|
}
|
|
}
|
|
|
|
func TestHoverChangesTranscriptRenderOutput(t *testing.T) {
|
|
m := specialistClickTestModel(t, "child-sess-1")
|
|
width := m.chatColumnWidth()
|
|
without := m.transcriptBodyLayout(width, "").String()
|
|
|
|
x, y, ok := specialistCardClickPoint(m, 2)
|
|
if !ok {
|
|
t.Fatal("expected a specialist card selectable line in the layout")
|
|
}
|
|
updated, _ := m.Update(testMouseMotion(tea.MouseNone, x, y))
|
|
m = updated.(model)
|
|
if m.hover.kind != hoverTranscript {
|
|
t.Fatal("sanity check failed: hover should be set over the specialist card")
|
|
}
|
|
with := m.transcriptBodyLayout(width, "").String()
|
|
|
|
if without == with {
|
|
t.Fatal("rendering with a hover target set should differ from without (the highlight should show)")
|
|
}
|
|
}
|
|
|
|
func TestHoverChangesSidebarRenderOutput(t *testing.T) {
|
|
m := swarmSidebarTestModel(t, map[string]string{"subagent-1": "sess-1"})
|
|
width := m.chatColumnWidth()
|
|
height := m.height
|
|
without := strings.Join(m.renderContextSidebar(sidebarWidth(m.width), height), "\n")
|
|
|
|
hits := m.sidebarAgentSelectables(sidebarWidth(m.width))
|
|
if len(hits) == 0 {
|
|
t.Fatal("expected at least one clickable sidebar agent row")
|
|
}
|
|
updated, _ := m.Update(testMouseMotion(tea.MouseNone, width+3+1, hits[0].lineOffset))
|
|
m = updated.(model)
|
|
if m.hover.kind != hoverSidebarAgent {
|
|
t.Fatal("sanity check failed: hover should be set over the sidebar agent row")
|
|
}
|
|
with := strings.Join(m.renderContextSidebar(sidebarWidth(m.width), height), "\n")
|
|
|
|
if without == with {
|
|
t.Fatal("sidebar rendering with a hover target set should differ from without (the highlight should show)")
|
|
}
|
|
}
|
|
|
|
func TestHoverClearsOnWheelScroll(t *testing.T) {
|
|
m := mouseTestModel()
|
|
for i := 0; i < 80; i++ {
|
|
m.transcript = appendRow(m.transcript, rowUser, "line content")
|
|
}
|
|
m.hover = hoverTarget{kind: hoverTranscript, bodyY: 5}
|
|
|
|
updated, _ := m.Update(testMouseWheel(tea.MouseWheelUp, 0, 3))
|
|
m = updated.(model)
|
|
if m.hover.kind != hoverNone {
|
|
t.Fatalf("hover should clear on a wheel scroll, got %#v", m.hover)
|
|
}
|
|
}
|
|
|
|
func TestHoverClearsOnSubchatExit(t *testing.T) {
|
|
m := mouseTestModel()
|
|
m.subchat.active = true
|
|
m.subchat.childSessionID = "child-1"
|
|
m.subchat.childRows = appendRow(nil, rowUser, "child text")
|
|
m.hover = hoverTarget{kind: hoverTranscript, bodyY: 5}
|
|
|
|
updated, _ := m.Update(tea.KeyPressMsg(tea.Key{Code: tea.KeyEsc}))
|
|
m = updated.(model)
|
|
if m.subchat.active {
|
|
t.Fatal("Esc should exit the subchat view")
|
|
}
|
|
if m.hover.kind != hoverNone {
|
|
t.Fatalf("hover should clear on subchat exit (bodyY numbering differs), got %#v", m.hover)
|
|
}
|
|
}
|
|
|
|
// A sidebar row can disappear between when it was hovered and the next render
|
|
// (a swarm member's linger window elapsing) with no mouse motion in between to
|
|
// re-target the hover. hoveredSidebarLineOffset must not paint a highlight on
|
|
// whatever unrelated row now occupies that identity's old slot — it must find
|
|
// nothing and skip painting entirely.
|
|
func TestHoveredSidebarLineOffsetSelfHealsWhenRowDisappears(t *testing.T) {
|
|
m := swarmSidebarTestModel(t, map[string]string{"subagent-1": "sess-1"})
|
|
hits := m.sidebarAgentSelectables(sidebarWidth(m.width))
|
|
if len(hits) == 0 {
|
|
t.Fatal("expected at least one clickable sidebar agent row")
|
|
}
|
|
m.hover = hoverTarget{kind: hoverSidebarAgent, sessionID: hits[0].sessionID}
|
|
|
|
// Simulate the member disappearing (a fresh model with no swarm member at all,
|
|
// same identity no longer present) without any mouse motion clearing m.hover.
|
|
m2 := swarmSidebarTestModel(t, nil)
|
|
m2.hover = m.hover
|
|
|
|
if _, ok := m2.hoveredSidebarLineOffset(sidebarWidth(m2.width)); ok {
|
|
t.Fatal("hoveredSidebarLineOffset should find nothing once the identity no longer resolves, not fall back to a stale/coincidental row")
|
|
}
|
|
}
|