1package daemon
2
3import (
4 "time"
5
6 "github.com/bnema/vev/internal/domain"
7)
8
9// viewOptions controls optional (costly) fields captured by snapshotView.
10type viewOptions struct {
11 tabDetails bool
12 focusedTitles bool
13 terminalTitle bool
14}
15
16// tabView is an immutable, value-only description of one tab.
17type tabView struct {
18 id domain.TabStableID
19 name string
20 focusedTitle string
21 attention bool
22 attentionAt time.Time
23}
24
25// sessionView is an immutable, value-only description of a session. It
26// deliberately retains no live pointers, mirroring route snapshot entries.
27// Listing paths (picker, palette, and list-sessions) read local session state
28// through snapshotView.
29type sessionView struct {
30 id domain.SessionID
31 incarnation domain.IncarnationID
32 name string
33 ephemeral bool
34 createdAt int64
35 defaultTab int
36 mruAt uint64
37 attached bool
38 tabCount int
39 hasAttention bool
40 tabs []tabView
41}
42
43// sessionInteractivelyAttachedLocked reports whether sess has registered
44// interactive presence: at least one attachment that is active or has an
45// activation in progress. Suspended attachments keep transport ownership and
46// session membership but hold no interactive authority, so they do not count.
47// Caller holds s.mu; each activity read briefly takes only that attachment's
48// lifecycle.mu and never freezes, drains, or waits on a gate.
49func sessionInteractivelyAttachedLocked(s *session) bool {
50 if s == nil {
51 return false
52 }
53 for ac := range s.attachments {
54 switch ac.attachmentActivity() {
55 case attachmentActive, attachmentActivating:
56 return true
57 }
58 }
59 return false
60}
61
62// snapshotView reads session fields under s.mu and samples the independently
63// atomic mruAt while that lock is held. tabCount and hasAttention are always
64// filled; tabs is allocated only when opts.tabDetails is true (non-nil empty
65// slice when there are no tabs). Never call while holding a pane lock.
66func (s *session) snapshotView(opts viewOptions) sessionView {
67 if opts.focusedTitles && !opts.tabDetails {
68 opts.tabDetails = true
69 }
70 s.mu.Lock()
71 // Session-level listings use the first ordered tab as their deterministic
72 // default. Interactive callers pass an attachment and resolve its stable
73 // view separately.
74 view := sessionView{
75 id: s.id,
76 incarnation: s.incarnation,
77 name: s.name,
78 ephemeral: s.ephemeral,
79 createdAt: s.createdAt,
80 defaultTab: 0,
81 mruAt: s.mruAt.Load(),
82 // attached reports interactive presence only; a suspended-only session
83 // remains registered for teardown and possible activation but is not
84 // observably attached.
85 attached: sessionInteractivelyAttachedLocked(s),
86 tabCount: len(s.tabs),
87 }
88 if opts.tabDetails {
89 view.tabs = make([]tabView, 0, len(s.tabs))
90 }
91 for i, tb := range s.tabs {
92 if tb.attention {
93 view.hasAttention = true
94 if !opts.tabDetails {
95 break
96 }
97 }
98 if !opts.tabDetails {
99 continue
100 }
101 entry := tabView{
102 id: domain.TabStableID(tb.stableID),
103 name: tabDisplayName(tb, i),
104 attention: tb.attention,
105 attentionAt: tb.attentionAt,
106 }
107 if opts.focusedTitles {
108 entry.focusedTitle = tb.focusedPaneTitle(opts.terminalTitle)
109 }
110 view.tabs = append(view.tabs, entry)
111 }
112 s.mu.Unlock()
113 return view
114}
115
116// pickerView renders this snapshot as the picker's daemon-private value type.
117func (view sessionView) pickerView() pickerSessionView {
118 out := pickerSessionView{
119 ID: view.id,
120 Incarnation: view.incarnation,
121 Name: view.name,
122 TargetName: view.name,
123 Active: view.defaultTab,
124 Tabs: make([]pickerTabEntry, 0, len(view.tabs)),
125 }
126 if !view.ephemeral {
127 createdAt := view.createdAt
128 out.ExpectedCreatedAt = &createdAt
129 }
130 attention := false
131 for _, tb := range view.tabs {
132 out.Tabs = append(out.Tabs, pickerTabEntry{
133 TabID: tb.id,
134 Name: tb.name,
135 Detail: tabTitleDetail(tb.name, tb.focusedTitle),
136 Attention: tb.attention,
137 })
138 if tb.attention {
139 attention = true
140 }
141 }
142 if attention {
143 out.Name = attentionSuffix(out.Name)
144 }
145 return out
146}