@@ -119,10 +119,12 @@ type Model struct {
119119 toolTotal int // cumulative tool calls this session
120120 sessionStart time.Time // first-prompt timestamp, for session wall-clock
121121
122- status string
123- notices []string
124- disconn bool
125- quitting bool
122+ status string
123+ notices []string
124+ noticeExp []time.Time // parallel to notices; zero = sticky, else expires at
125+ noticeSeq int // bumped on each transient notice, to invalidate stale timers
126+ disconn bool
127+ quitting bool
126128
127129 gradRule string // cached full-width gradient rule
128130 gradRuleW int
@@ -164,6 +166,15 @@ func (a autocomplete) height() int {
164166 return a .rows () + 3
165167}
166168
169+ // noticeTTL is how long transient info traces (skill / memory / signal /
170+ // subagent) stay on screen before fading out.
171+ const noticeTTL = 5 * time .Second
172+
173+ // noticeExpireMsg fires noticeTTL after the last transient notice was added.
174+ type noticeExpireMsg struct {
175+ seq int
176+ }
177+
167178// acResultMsg carries the result of an async resource search.
168179type acResultMsg struct {
169180 seq int
@@ -274,6 +285,13 @@ func (m *Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
274285 case eventMsg :
275286 return m .handleEvent (client .Event (msg ))
276287
288+ case noticeExpireMsg :
289+ if msg .seq == m .noticeSeq {
290+ m .pruneNotices (time .Now ())
291+ m .refresh ()
292+ }
293+ return m , nil
294+
277295 case tea.MouseMsg :
278296 var cmd tea.Cmd
279297 m .vp , cmd = m .vp .Update (msg )
@@ -397,6 +415,7 @@ func (m *Model) handleKey(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
397415}
398416
399417func (m * Model ) handleEvent (ev client.Event ) (tea.Model , tea.Cmd ) {
418+ prevSeq := m .noticeSeq
400419 switch ev .Type {
401420 case "session" :
402421 m .sessionID = ev .SessionID
@@ -496,11 +515,11 @@ func (m *Model) handleEvent(ev client.Event) (tea.Model, tea.Cmd) {
496515 m .relayout () // the panel is taller than the textarea — shrink the viewport
497516
498517 case "skill_event" :
499- m .addNote ("skill · " + strings .TrimSpace (ev .SubType + " " + ev .SkillName ) + eventTail (ev ))
518+ m .addTransientNote ("skill · " + strings .TrimSpace (ev .SubType + " " + ev .SkillName ) + eventTail (ev ))
500519 case "memory_event" :
501- m .addNote ("memory · " + strings .TrimSpace (ev .SubType + " " + ev .Target ) + eventTail (ev ))
520+ m .addTransientNote ("memory · " + strings .TrimSpace (ev .SubType + " " + ev .Target ) + eventTail (ev ))
502521 case "agent_signal" :
503- m .addNote ("signal · " + strings .TrimSpace (ev .SubType + " " + ev .Detail ) + eventTail (ev ))
522+ m .addTransientNote ("signal · " + strings .TrimSpace (ev .SubType + " " + ev .Detail ) + eventTail (ev ))
504523 case "subagent_log" :
505524 line := strings .TrimSpace (ev .SubType + " " + ev .Name )
506525 if d := collapse (ev .Detail ); d != "" {
@@ -512,7 +531,7 @@ func (m *Model) handleEvent(ev client.Event) (tea.Model, tea.Cmd) {
512531 if i := m .cur (); i >= 0 && m .attachSubLog (i , line ) {
513532 break
514533 }
515- m .addNote ("subagent · " + line )
534+ m .addTransientNote ("subagent · " + line )
516535
517536 case client .EventDisconnected :
518537 m .disconn = true
@@ -527,7 +546,7 @@ func (m *Model) handleEvent(ev client.Event) (tea.Model, tea.Cmd) {
527546 }
528547
529548 m .refresh ()
530- return m , listen (m .events )
549+ return m , tea . Batch ( listen (m .events ), m . noticeTimer ( prevSeq ) )
531550}
532551
533552// ── actions ──────────────────────────────────────────────────────────────
@@ -686,11 +705,51 @@ func (m *Model) finalize() {
686705 m .thinking .Reset ()
687706}
688707
708+ // addNote appends a sticky notice (errors, disconnects) that stays until
709+ // pushed out by newer ones.
689710func (m * Model ) addNote (s string ) {
711+ m .pushNote (s , time.Time {})
712+ }
713+
714+ // addTransientNote appends an info trace that fades after noticeTTL.
715+ func (m * Model ) addTransientNote (s string ) {
716+ m .pushNote (s , time .Now ().Add (noticeTTL ))
717+ m .noticeSeq ++
718+ }
719+
720+ func (m * Model ) pushNote (s string , exp time.Time ) {
690721 m .notices = append (m .notices , sanitize (s ))
722+ m .noticeExp = append (m .noticeExp , exp )
691723 if len (m .notices ) > 6 {
692724 m .notices = m .notices [len (m .notices )- 6 :]
725+ m .noticeExp = m .noticeExp [len (m .noticeExp )- 6 :]
726+ }
727+ }
728+
729+ // pruneNotices drops transient notices whose expiry has passed.
730+ func (m * Model ) pruneNotices (now time.Time ) {
731+ kept := m .notices [:0 ]
732+ keptExp := m .noticeExp [:0 ]
733+ for i , n := range m .notices {
734+ if exp := m .noticeExp [i ]; exp .IsZero () || now .Before (exp ) {
735+ kept = append (kept , n )
736+ keptExp = append (keptExp , exp )
737+ }
738+ }
739+ m .notices = kept
740+ m .noticeExp = keptExp
741+ }
742+
743+ // noticeTimer schedules the expiry sweep when a transient notice was added
744+ // since prevSeq; otherwise it returns nil.
745+ func (m * Model ) noticeTimer (prevSeq int ) tea.Cmd {
746+ if m .noticeSeq == prevSeq {
747+ return nil
693748 }
749+ seq := m .noticeSeq
750+ return tea .Tick (noticeTTL , func (time.Time ) tea.Msg {
751+ return noticeExpireMsg {seq : seq }
752+ })
694753}
695754
696755// render runs content through glamour; falls back to raw text on error.
0 commit comments