Skip to content

Commit 672101e

Browse files
authored
Merge pull request #2544 from dgageot/board/simplifying-custom-tui-component-5a41ea99
refactor(tui): simplify components, drop dead code, consolidate helpers
2 parents 3bc200c + 58e478d commit 672101e

19 files changed

Lines changed: 135 additions & 309 deletions

File tree

pkg/tui/components/markdown/fast_renderer_test.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ import (
66
"strings"
77
"testing"
88

9+
"charm.land/glamour/v2"
910
"github.com/charmbracelet/x/ansi"
1011
runewidth "github.com/mattn/go-runewidth"
1112
"github.com/stretchr/testify/assert"
1213
"github.com/stretchr/testify/require"
14+
15+
"github.com/docker/docker-agent/pkg/tui/styles"
1316
)
1417

1518
// stripANSI removes ANSI escape sequences from a string.
@@ -19,6 +22,19 @@ func stripANSI(s string) string {
1922
return ansiRegex.ReplaceAllString(s, "")
2023
}
2124

25+
// newGlamourRenderer creates a markdown renderer using glamour.
26+
// Used as a reference implementation to compare against the fast renderer.
27+
func newGlamourRenderer(width int) *glamour.TermRenderer {
28+
style := styles.MarkdownStyle()
29+
30+
r, _ := glamour.NewTermRenderer(
31+
glamour.WithWordWrap(width),
32+
glamour.WithStyles(style),
33+
glamour.WithPreservedNewLines(),
34+
)
35+
return r
36+
}
37+
2238
func TestFastRendererBasicText(t *testing.T) {
2339
t.Parallel()
2440

@@ -909,7 +925,7 @@ func BenchmarkFastRenderer(b *testing.B) {
909925
}
910926

911927
func BenchmarkGlamourRenderer(b *testing.B) {
912-
r := NewGlamourRenderer(80)
928+
r := newGlamourRenderer(80)
913929
for b.Loop() {
914930
_, _ = r.Render(benchmarkInput)
915931
}
@@ -924,7 +940,7 @@ func BenchmarkFastRendererSmall(b *testing.B) {
924940
}
925941

926942
func BenchmarkGlamourRendererSmall(b *testing.B) {
927-
r := NewGlamourRenderer(80)
943+
r := newGlamourRenderer(80)
928944
input := "Hello **world**, this is a *test*."
929945
for b.Loop() {
930946
_, _ = r.Render(input)
@@ -940,7 +956,7 @@ func BenchmarkFastRendererCodeBlock(b *testing.B) {
940956
}
941957

942958
func BenchmarkGlamourRendererCodeBlock(b *testing.B) {
943-
r := NewGlamourRenderer(80)
959+
r := newGlamourRenderer(80)
944960
input := "```go\nfunc main() {\n\tfmt.Println(\"hello`\")\n}\n```"
945961
for b.Loop() {
946962
_, _ = r.Render(input)
@@ -962,7 +978,7 @@ func BenchmarkFastRendererTable(b *testing.B) {
962978
}
963979

964980
func BenchmarkGlamourRendererTable(b *testing.B) {
965-
r := NewGlamourRenderer(80)
981+
r := newGlamourRenderer(80)
966982
for b.Loop() {
967983
_, _ = r.Render(benchmarkTableInput)
968984
}
@@ -976,7 +992,7 @@ func BenchmarkFastRendererTableWidth20(b *testing.B) {
976992
}
977993

978994
func BenchmarkGlamourRendererTableWidth20(b *testing.B) {
979-
r := NewGlamourRenderer(20)
995+
r := newGlamourRenderer(20)
980996
for b.Loop() {
981997
_, _ = r.Render(benchmarkTableInput)
982998
}
@@ -990,7 +1006,7 @@ func BenchmarkFastRendererTableWidth200(b *testing.B) {
9901006
}
9911007

9921008
func BenchmarkGlamourRendererTableWidth200(b *testing.B) {
993-
r := NewGlamourRenderer(200)
1009+
r := newGlamourRenderer(200)
9941010
for b.Loop() {
9951011
_, _ = r.Render(benchmarkTableInput)
9961012
}
@@ -1734,7 +1750,7 @@ func BenchmarkStreamingGlamourRenderer(b *testing.B) {
17341750

17351751
b.ResetTimer()
17361752
for b.Loop() {
1737-
r := NewGlamourRenderer(80)
1753+
r := newGlamourRenderer(80)
17381754
var accumulated strings.Builder
17391755
for _, chunk := range chunks {
17401756
accumulated.WriteString(chunk)
Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
11
package markdown
22

3-
import (
4-
"charm.land/glamour/v2"
5-
6-
"github.com/docker/docker-agent/pkg/tui/styles"
7-
)
8-
93
// Renderer is an interface for markdown renderers.
104
type Renderer interface {
115
Render(input string) (string, error)
@@ -15,16 +9,3 @@ type Renderer interface {
159
func NewRenderer(width int) Renderer {
1610
return NewFastRenderer(width)
1711
}
18-
19-
// NewGlamourRenderer creates a markdown renderer using glamour.
20-
// This is kept for compatibility and testing purposes.
21-
func NewGlamourRenderer(width int) *glamour.TermRenderer {
22-
style := styles.MarkdownStyle()
23-
24-
r, _ := glamour.NewTermRenderer(
25-
glamour.WithWordWrap(width),
26-
glamour.WithStyles(style),
27-
glamour.WithPreservedNewLines(),
28-
)
29-
return r
30-
}

pkg/tui/components/message/message.go

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package message
22

33
import (
44
"fmt"
5-
"regexp"
65
"strings"
76

87
tea "charm.land/bubbletea/v2"
@@ -249,12 +248,6 @@ func (mv *messageModel) GetSize() (width, height int) {
249248
return mv.width, mv.height
250249
}
251250

252-
var ansiEscape = regexp.MustCompile("\x1b\\[[0-9;]*m")
253-
254-
func stripANSI(s string) string {
255-
return ansiEscape.ReplaceAllString(s, "")
256-
}
257-
258251
// preserveLineBreaks preserves leading indentation by converting leading spaces
259252
// to non-breaking spaces (U+00A0) which won't be stripped by markdown parsers.
260253
// Line breaks are handled by glamour.WithPreservedNewLines().

pkg/tui/components/message/message_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package message
22

33
import (
4+
"regexp"
45
"strings"
56
"testing"
67

@@ -10,6 +11,12 @@ import (
1011
"github.com/docker/docker-agent/pkg/tui/types"
1112
)
1213

14+
var ansiEscape = regexp.MustCompile("\x1b\\[[0-9;]*m")
15+
16+
func stripANSI(s string) string {
17+
return ansiEscape.ReplaceAllString(s, "")
18+
}
19+
1320
func TestErrorMessageWrapping(t *testing.T) {
1421
t.Parallel()
1522

pkg/tui/components/messages/messages.go

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -236,9 +236,6 @@ func (m *model) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
236236
}
237237
return m, tea.Batch(cmds...)
238238

239-
case reasoningblock.BlockMsg:
240-
return m.forwardToReasoningBlock(msg.GetBlockID(), msg)
241-
242239
case animation.TickMsg:
243240
// Invalidate render cache if there's animated content that needs redrawing.
244241
// This ensures fades, spinners, etc. actually update visually on each tick.
@@ -1142,21 +1139,6 @@ func (m *model) invalidateAllItems() {
11421139
m.renderDirty = true
11431140
}
11441141

1145-
// forwardToReasoningBlock finds the reasoning block with the given ID and forwards the message to it.
1146-
func (m *model) forwardToReasoningBlock(blockID string, msg tea.Msg) (layout.Model, tea.Cmd) {
1147-
for i, tuiMsg := range m.messages {
1148-
if tuiMsg.Type == types.MessageTypeAssistantReasoningBlock {
1149-
if block, ok := m.views[i].(*reasoningblock.Model); ok && block.ID() == blockID {
1150-
updatedView, cmd := m.views[i].Update(msg)
1151-
m.views[i] = updatedView
1152-
m.invalidateItem(i)
1153-
return m, cmd
1154-
}
1155-
}
1156-
}
1157-
return m, nil
1158-
}
1159-
11601142
// Message management methods
11611143
func (m *model) AddUserMessage(content string) tea.Cmd {
11621144
return m.addMessage(types.User(content))

pkg/tui/components/reasoningblock/reasoningblock.go

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -49,21 +49,6 @@ func fadeColor(progress float64) color.Color {
4949
// Tests can override this for deterministic behavior.
5050
var nowFunc = time.Now
5151

52-
// BlockMsg is implemented by messages that target a specific reasoning block.
53-
type BlockMsg interface {
54-
GetBlockID() string
55-
}
56-
57-
// blockMsgBase is embedded in messages that target a specific reasoning block.
58-
type blockMsgBase struct {
59-
BlockID string
60-
}
61-
62-
func (m blockMsgBase) GetBlockID() string { return m.BlockID }
63-
64-
// ToggleMsg is sent when the block should toggle expanded/collapsed state.
65-
type ToggleMsg struct{ blockMsgBase }
66-
6752
// toolEntry holds a tool call message and its view.
6853
type toolEntry struct {
6954
msg *types.Message

pkg/tui/components/scrollview/scrollview.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,6 @@ func ReadOnlyScrollKeyMap() *ScrollKeyMap {
5353

5454
type Option func(*Model)
5555

56-
// WithGapWidth sets the space columns between content and scrollbar (default 1).
57-
func WithGapWidth(n int) Option { return func(m *Model) { m.gapWidth = n } }
58-
5956
// WithReserveScrollbarSpace always reserves gap+scrollbar columns, preventing layout shifts.
6057
func WithReserveScrollbarSpace(v bool) Option {
6158
return func(m *Model) { m.reserveScrollbarSpace = v }

pkg/tui/components/sidebar/sidebar.go

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -154,15 +154,8 @@ type model struct {
154154
agentClickZones map[int]string // content line -> agent name
155155
}
156156

157-
// Option is a functional option for configuring the sidebar.
158-
type Option func(*model)
159-
160-
// WithLayoutConfig sets a custom layout configuration.
161-
func WithLayoutConfig(cfg LayoutConfig) Option {
162-
return func(m *model) { m.layoutCfg = cfg }
163-
}
164-
165-
func New(sessionState *service.SessionState, opts ...Option) Model {
157+
// New creates a new sidebar bound to the given session state.
158+
func New(sessionState *service.SessionState) Model {
166159
ti := textinput.New()
167160
ti.Placeholder = "Session title"
168161
ti.CharLimit = 50
@@ -188,9 +181,6 @@ func New(sessionState *service.SessionState, opts ...Option) Model {
188181
titleInput: ti,
189182
cacheDirty: true, // Initial render needed
190183
}
191-
for _, opt := range opts {
192-
opt(m)
193-
}
194184
return m
195185
}
196186

pkg/tui/components/tool/directorytree/directorytree.go

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package directorytree
22

33
import (
4-
"fmt"
54
"strings"
65

76
"github.com/docker/docker-agent/pkg/tools/builtin"
@@ -27,18 +26,16 @@ func extractResult(msg *types.Message) string {
2726
return ""
2827
}
2928

30-
fileCount := meta.FileCount
31-
dirCount := meta.DirCount
32-
if fileCount+dirCount == 0 {
29+
if meta.FileCount+meta.DirCount == 0 {
3330
return "empty"
3431
}
3532

3633
var parts []string
37-
if fileCount > 0 {
38-
parts = append(parts, formatCount(fileCount, "file", "files"))
34+
if meta.FileCount > 0 {
35+
parts = append(parts, toolcommon.Pluralize(meta.FileCount, "file", "files"))
3936
}
40-
if dirCount > 0 {
41-
parts = append(parts, formatCount(dirCount, "dir", "dirs"))
37+
if meta.DirCount > 0 {
38+
parts = append(parts, toolcommon.Pluralize(meta.DirCount, "dir", "dirs"))
4239
}
4340

4441
result := strings.Join(parts, ", ")
@@ -47,10 +44,3 @@ func extractResult(msg *types.Message) string {
4744
}
4845
return result
4946
}
50-
51-
func formatCount(count int, singular, plural string) string {
52-
if count == 1 {
53-
return fmt.Sprintf("%d %s", count, singular)
54-
}
55-
return fmt.Sprintf("%d %s", count, plural)
56-
}

0 commit comments

Comments
 (0)