44 "sort"
55 "strings"
66
7+ "charm.land/bubbles/v2/viewport"
78 tea "charm.land/bubbletea/v2"
89
910 "github.com/kincoy/cc9s/internal/claudefs"
@@ -21,10 +22,12 @@ const (
2122
2223// AgentListModel agent list view model.
2324type AgentListModel struct {
24- state DefaultResourceListState [claudefs.AgentResource ]
25- loadErr error
26- sortBy AgentSortField
27- sortAsc bool
25+ state DefaultResourceListState [claudefs.AgentResource ]
26+ loadErr error
27+ sortBy AgentSortField
28+ sortAsc bool
29+ viewport viewport.Model
30+ lastWidth int
2831}
2932
3033type agentsLoadedMsg struct {
@@ -40,7 +43,11 @@ func NewAgentListModel() *AgentListModel {
4043}
4144
4245func (m * AgentListModel ) Init () tea.Cmd {
43- return scanAgentsCmd
46+ m .viewport = NewViewportWithSize (80 , 20 ) // default size, updated on WindowSizeMsg
47+ return tea .Batch (
48+ m .viewport .Init (),
49+ scanAgentsCmd ,
50+ )
4451}
4552
4653func scanAgentsCmd () tea.Msg {
@@ -49,11 +56,24 @@ func scanAgentsCmd() tea.Msg {
4956
5057func (m * AgentListModel ) Update (msg tea.Msg ) tea.Cmd {
5158 switch msg := msg .(type ) {
59+ case tea.WindowSizeMsg :
60+ m .viewport .SetWidth (msg .Width )
61+ // Body height = total - header (3) - footer (1)
62+ // Body height = total - header(3) - tabs(2) - footer(1)
63+ bodyHeight := msg .Height - 6
64+ if bodyHeight < 1 {
65+ bodyHeight = 1
66+ }
67+ m .viewport .SetHeight (bodyHeight )
68+ m .updateViewportContent ()
69+
5270 case agentsLoadedMsg :
5371 m .loadErr = msg .result .Err
5472 items := append ([]claudefs.AgentResource (nil ), msg .result .Agents ... )
5573 m .sortAgents (items )
5674 m .state .SetItems (items , m .agentHooks ())
75+ m .updateViewportContent ()
76+ return func () tea.Msg { return StopLoadingMsg {Resource : ResourceAgents } }
5777
5878 case tea.KeyPressMsg :
5979 if m .loadErr != nil {
@@ -64,25 +84,88 @@ func (m *AgentListModel) Update(msg tea.Msg) tea.Cmd {
6484 case "j" , "down" :
6585 if m .state .Cursor < len (m .state .VisibleItems )- 1 {
6686 m .state .Cursor ++
87+ m .updateViewportContent ()
88+ EnsureLineVisible (& m .viewport , m .state .Cursor )
6789 }
6890 case "k" , "up" :
6991 if m .state .Cursor > 0 {
7092 m .state .Cursor --
93+ m .updateViewportContent ()
94+ EnsureLineVisible (& m .viewport , m .state .Cursor )
7195 }
7296 case "G" :
7397 if len (m .state .VisibleItems ) > 0 {
7498 m .state .Cursor = len (m .state .VisibleItems ) - 1
99+ m .updateViewportContent ()
100+ EnsureLineVisible (& m .viewport , m .state .Cursor )
75101 }
76102 case "g" :
77103 m .state .Cursor = 0
104+ m .updateViewportContent ()
105+ EnsureLineVisible (& m .viewport , m .state .Cursor )
106+
107+ // Half-page and full-page navigation
108+ case "ctrl+d" :
109+ halfPage := m .viewport .Height () / 2
110+ if halfPage < 1 {
111+ halfPage = 1
112+ }
113+ m .state .Cursor += halfPage
114+ if m .state .Cursor >= len (m .state .VisibleItems ) {
115+ m .state .Cursor = len (m .state .VisibleItems ) - 1
116+ }
117+ if m .state .Cursor < 0 {
118+ m .state .Cursor = 0
119+ }
120+ m .updateViewportContent ()
121+ EnsureLineVisible (& m .viewport , m .state .Cursor )
122+ case "ctrl+u" :
123+ halfPage := m .viewport .Height () / 2
124+ if halfPage < 1 {
125+ halfPage = 1
126+ }
127+ m .state .Cursor -= halfPage
128+ if m .state .Cursor < 0 {
129+ m .state .Cursor = 0
130+ }
131+ m .updateViewportContent ()
132+ EnsureLineVisible (& m .viewport , m .state .Cursor )
133+ case "pgdown" :
134+ fullPage := m .viewport .Height ()
135+ if fullPage < 1 {
136+ fullPage = 1
137+ }
138+ m .state .Cursor += fullPage
139+ if m .state .Cursor >= len (m .state .VisibleItems ) {
140+ m .state .Cursor = len (m .state .VisibleItems ) - 1
141+ }
142+ if m .state .Cursor < 0 {
143+ m .state .Cursor = 0
144+ }
145+ m .updateViewportContent ()
146+ EnsureLineVisible (& m .viewport , m .state .Cursor )
147+ case "pgup" :
148+ fullPage := m .viewport .Height ()
149+ if fullPage < 1 {
150+ fullPage = 1
151+ }
152+ m .state .Cursor -= fullPage
153+ if m .state .Cursor < 0 {
154+ m .state .Cursor = 0
155+ }
156+ m .updateViewportContent ()
157+ EnsureLineVisible (& m .viewport , m .state .Cursor )
158+
78159 case "s" :
79160 m .sortBy = (m .sortBy + 1 ) % 3
80161 m .sortAgents (m .state .AllItems )
81162 m .applyContext ()
163+ m .updateViewportContent ()
82164 case "S" :
83165 m .sortAsc = ! m .sortAsc
84166 m .sortAgents (m .state .AllItems )
85167 m .applyContext ()
168+ m .updateViewportContent ()
86169 case "d" :
87170 if len (m .state .VisibleItems ) > 0 {
88171 return func () tea.Msg {
@@ -107,6 +190,7 @@ func (m *AgentListModel) GetContext() Context {
107190
108191func (m * AgentListModel ) SetContext (ctx Context ) tea.Cmd {
109192 m .state .SetContext (ctx , m .agentHooks ())
193+ m .updateViewportContent ()
110194 return nil
111195}
112196
@@ -118,6 +202,8 @@ func (m *AgentListModel) Reload() tea.Cmd {
118202}
119203
120204func (m * AgentListModel ) View (width , height int ) string {
205+ m .lastWidth = width
206+
121207 if m .state .Loading {
122208 return renderCenteredText ("Loading agents..." , width , height )
123209 }
@@ -130,11 +216,42 @@ func (m *AgentListModel) View(width, height int) string {
130216 }
131217 return renderCenteredText ("No agents found" , width , height )
132218 }
133- return renderAgentTable (m .state .VisibleItems , m .state .Cursor , width , height , m .ShowProjectColumn (), m .sortBy , m .sortAsc )
219+
220+ return m .viewport .View ()
221+ }
222+
223+ // updateViewportContent updates the viewport with rendered agent table content
224+ func (m * AgentListModel ) updateViewportContent () {
225+ if m .state .Loading || m .loadErr != nil || len (m .state .VisibleItems ) == 0 {
226+ return
227+ }
228+
229+ // Prefer lastWidth (actual rendering width from View()) over viewport width
230+ width := m .lastWidth
231+ if width == 0 {
232+ width = m .viewport .Width ()
233+ }
234+ if width == 0 {
235+ width = 80 // default width if not yet sized
236+ }
237+ height := m .viewport .Height ()
238+ if height == 0 {
239+ height = 20 // default height if not yet sized
240+ }
241+
242+ contextLabel := ""
243+ if m .state .Context .Type == ContextAll {
244+ contextLabel = "All Agents"
245+ } else if m .state .Context .Type == ContextProject {
246+ contextLabel = m .state .Context .Value
247+ }
248+ content := renderAgentTable (m .state .VisibleItems , m .state .Cursor , width , height , m .ShowProjectColumn (), m .sortBy , m .sortAsc , contextLabel )
249+ m .viewport .SetContent (content )
134250}
135251
136252func (m * AgentListModel ) ApplyFilter (query string ) {
137253 m .state .ApplyFilter (query , m .agentHooks ())
254+ m .updateViewportContent ()
138255}
139256
140257func (m * AgentListModel ) ShowProjectColumn () bool {
0 commit comments