Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion internal/tui/panels.go
Original file line number Diff line number Diff line change
Expand Up @@ -989,10 +989,15 @@ func (m *Model) handleSessionDetail(msg sessionDetailMsg) tea.Cmd {
m.lastLatency = 0
m.msgs = m.msgs[:0]
m.convCount = -1 // transcript swapped for the resumed one — drop the cache
// The plan surface is session-scoped knowledge: drop it and refetch for
// the resumed session. Neither live path covers a resume — the replayed
// transcript bypasses the WS trigger, and the adopt's session event
// carries the already-swapped id (no switch delta → no reset).
m.resetPlanState()
m.replayTranscript(msg.sess.Messages)
note := m.transientNoteCmd("resumed session " + shortID(msg.sess.ID))
m.closePanel()
return tea.Batch(note, m.adoptSession())
return tea.Batch(note, m.adoptSession(), m.fetchPlan())
}

// handleSessionUpdated applies a pin/unpin outcome to the list in place.
Expand Down
41 changes: 41 additions & 0 deletions internal/tui/plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tui

import (
"errors"
"strings"
"testing"

"github.com/BackendStack21/bodek/internal/client"
Expand All @@ -20,6 +21,46 @@ func planCallEvent(name string) client.Event {
return client.Event{Type: "tool_call", Name: name, Data: "{}"}
}

// TestPlanSurface_SessionResumeResetsAndRefetches pins the resume path:
// handleSessionDetail drops the previous session's plan knowledge and seeds
// a fresh fetch, so the strip reflects the resumed session instead of
// leaking the old one (and never showing anything until a live plan call).
func TestPlanSurface_SessionResumeResetsAndRefetches(t *testing.T) {
m := newTestModel()
m.cl = &client.Client{} // fetch closures are built but never executed
m.sessionID = "s1"
acceptPlan(m, planFixture())

// Resume an interrupted session: transcript swap + adopt.
cmd := m.handleSessionDetail(sessionDetailMsg{sess: client.Session{ID: "s2"}, token: "a2"})
if cmd == nil {
t.Fatal("resume must return the adopt/fetch command batch")
}
if m.planInit || m.planVer != 0 {
t.Fatalf("resume must drop stale plan knowledge: init=%v ver=%d", m.planInit, m.planVer)
}
if m.planReqSeq == 0 {
t.Fatal("resume must seed a plan fetch for the resumed session")
}

// Until the resumed session's snapshot arrives, the old plan must not
// leak into the strip even while busy.
m.busy = true
if got := m.planStripLabel(); got != "" {
t.Fatalf("stale pre-resume plan leaked into the strip: %q", got)
}

snap := planFixture()
snap.SessionID = "s2"
acceptPlan(m, snap)
got := m.planStripLabel()
for _, want := range []string{"plan 1/4", "wire flag parsing"} {
if !strings.Contains(got, want) {
t.Errorf("resumed-session strip %q missing %q", got, want)
}
}
}

func TestPlanWSTrigger_DebouncedRefresh(t *testing.T) {
m := &Model{}
before := m.planDebSeq
Expand Down