tui: no signoff for a turn a note opened that the agent never answered
a /link or /work note commits just before your next message and opened a turn of its own, which your message then closed with an empty face and copy mark. Signed-off-by: dawn <90008@klbr.net>
69 unmodified lines707172737475767778106 unmodified linesExpand all18518618718818919019119219349 unmodified lines24324424524624724824925025125225325 unmodified lines2792802812822832842852862872882896 unmodified lines69 unmodified lines if text == "" { return nil } t.turnIsLive() t.turn.answered = true var entries []HistoryEntry if t.activeKind != kind { entries = t.settle(agentName) t.activeKind = kind106 unmodified linesExpand all entries = append(entries, HistoryEntry{Kind: EntryAssistant, Speaker: agentName, Text: evt.Text, Timestamp: ts}) if t.turn == nil { t.turn = newOpenTurn(ts) } t.turn.answered = true t.turn.touch(evt.Timestamp) case daemon.EventNote: entries = t.settle(agentName) entries = append(entries, HistoryEntry{Kind: EntryNote, Text: evt.Text, Timestamp: time.Now().UnixMilli()})49 unmodified lines tools int // A live turn ends now, rather than at its last replayed event. live bool failed bool // answered is whether the agent wrote anything; a turn opened by a note // it never answered has nothing to sign off. answered bool}
func newOpenTurn(ts int64) *openTurn { if ts == 0 {25 unmodified lines if turn == nil { return nil } t.turn, t.lastEvent = nil, time.Time{} if !turn.answered && turn.tools == 0 && !turn.failed && !stopped { return nil } end := turn.last if turn.live { end = max(end, time.Now().UnixMilli()) }6 unmodified lines123412345678910111213106 unmodified linesExpand all120121122123124125126127128129130131132133134135136137138139// These tests catch mutable live-buffer ownership and chunk-boundary regressions.// Daemon E2E cannot retain Go model copies or control the exact delta boundaries// received by the terminal. Copies here are retained readers; only the current// update path appends, settles, or discards live text.// These tests catch mutable live-buffer ownership and chunk-boundary// regressions, and signoffs placed by event order. Daemon E2E cannot retain Go// model copies, control the exact delta boundaries received by the terminal,// or see rows. Copies here are retained readers; only the current update path// appends, settles, or discards live text.package tui
import ( "slices" "strings" "testing" "unsafe"
106 unmodified linesExpand all if len(entries) != 1 || entries[0].Text != first+second { t.Fatal("deduplication did not reset after the committed message") }}
func TestANoteTheAgentNeverAnsweredHasNoSignoff(t *testing.T) { // /link and /work queue a note; it commits right before your next message. entries := replayTranscript([]daemon.StreamEvent{ {Type: daemon.EventUser, Source: "links", Text: "linked with chernobog:~/proj/albedo", Replayed: true}, {Type: daemon.EventUser, Source: "chat", Text: "hi", Replayed: true}, {Type: daemon.EventMessage, Text: "hello", Replayed: true}, }, "albedo") var kinds []EntryKind for _, entry := range entries { kinds = append(kinds, entry.Kind) } if want := []EntryKind{EntryUser, EntryUser, EntryAssistant}; !slices.Equal(kinds, want) { t.Fatalf("entries %v, want %v", kinds, want) }}