Skip to content

Commit 37a92f0

Browse files
authored
Merge pull request #27 from kincoy/feat/ui-infrastructure-refactor
Feat/UI infrastructure refactor
2 parents da08662 + 1e46a9d commit 37a92f0

24 files changed

+1491
-204
lines changed

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,7 @@ specs/
1919

2020
docs/
2121
community/
22-
AGENTS.md
22+
AGENTS.md
23+
24+
# Worktrees
25+
.worktrees/

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<p align="center">
88
<a href="https://go.dev"><img src="https://img.shields.io/badge/Go-1.25%2B-00ADD8?logo=go" alt="Go version"></a>
99
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License"></a>
10-
<a href="https://github.com/kincoy/cc9s/releases"><img src="https://img.shields.io/badge/Release-v0.2.1-green.svg" alt="Release"></a>
10+
<a href="https://github.com/kincoy/cc9s/releases"><img src="https://img.shields.io/badge/Release-v0.2.3-green.svg" alt="Release"></a>
1111
</p>
1212

1313
<p align="center">

README_zh.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<p align="center">
88
<a href="https://go.dev"><img src="https://img.shields.io/badge/Go-1.25%2B-00ADD8?logo=go" alt="Go version"></a>
99
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License"></a>
10-
<a href="https://github.com/kincoy/cc9s/releases"><img src="https://img.shields.io/badge/Release-v0.2.1-green.svg" alt="Release"></a>
10+
<a href="https://github.com/kincoy/cc9s/releases"><img src="https://img.shields.io/badge/Release-v0.2.3-green.svg" alt="Release"></a>
1111
</p>
1212

1313
<p align="center">

internal/ui/agent_list.go

Lines changed: 123 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
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.
2324
type 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

3033
type agentsLoadedMsg struct {
@@ -40,7 +43,11 @@ func NewAgentListModel() *AgentListModel {
4043
}
4144

4245
func (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

4653
func scanAgentsCmd() tea.Msg {
@@ -49,11 +56,24 @@ func scanAgentsCmd() tea.Msg {
4956

5057
func (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

108191
func (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

120204
func (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

136252
func (m *AgentListModel) ApplyFilter(query string) {
137253
m.state.ApplyFilter(query, m.agentHooks())
254+
m.updateViewportContent()
138255
}
139256

140257
func (m *AgentListModel) ShowProjectColumn() bool {

internal/ui/agent_table.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"github.com/kincoy/cc9s/internal/ui/styles"
1111
)
1212

13-
func renderAgentTable(agents []claudefs.AgentResource, cursor, width, height int, showProjectColumn bool, sortBy AgentSortField, sortAsc bool) string {
13+
func renderAgentTable(agents []claudefs.AgentResource, cursor, width, height int, showProjectColumn bool, sortBy AgentSortField, sortAsc bool, contextLabel string) string {
1414
if len(agents) == 0 {
1515
return ""
1616
}
@@ -22,7 +22,16 @@ func renderAgentTable(agents []claudefs.AgentResource, cursor, width, height int
2222
Foreground(styles.ColorWarning).
2323
Bold(true).
2424
Render(fmt.Sprintf("(%d)", len(agents)))
25-
title := resourceType + countStr
25+
26+
contextPart := ""
27+
if contextLabel != "" {
28+
contextPart = lipgloss.NewStyle().
29+
Foreground(styles.ColorPurple).
30+
Bold(true).
31+
Render(fmt.Sprintf("(%s)", contextLabel))
32+
}
33+
34+
title := resourceType + contextPart + countStr
2635
sb.WriteString(renderTopBorder(width, title))
2736

2837
contentWidth := width - 4
@@ -97,7 +106,7 @@ func renderAgentTable(agents []claudefs.AgentResource, cursor, width, height int
97106

98107
for i := startIdx; i < endIdx; i++ {
99108
agent := agents[i]
100-
rowStyle := styles.TableCellStyle
109+
rowStyle := styles.TableCellStyle.Faint(true)
101110
if i == cursor {
102111
rowStyle = styles.SelectedRowStyle
103112
}
@@ -135,7 +144,9 @@ func renderAgentTable(agents []claudefs.AgentResource, cursor, width, height int
135144
statusStyle.Inherit(rowStyle).Width(statusWidth).Render(styles.AgentStatusText(agent.Status)),
136145
)
137146

138-
sb.WriteString(renderRowBorder(lipgloss.JoinHorizontal(lipgloss.Top, rowParts...)))
147+
row := lipgloss.JoinHorizontal(lipgloss.Top, rowParts...)
148+
row = rowStyle.Width(contentWidth).Render(row)
149+
sb.WriteString(renderRowBorder(row))
139150
}
140151

141152
fillEmptyRows(&sb, contentWidth, endIdx-startIdx, visibleHeight)

0 commit comments

Comments
 (0)