Skip to content

Commit 4b0bcef

Browse files
authored
Merge pull request #1116 from dgageot/tui
Redesigned TUI
2 parents 989f18c + 1570105 commit 4b0bcef

34 files changed

Lines changed: 408 additions & 416 deletions

File tree

pkg/runtime/title_generator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func (t *titleGenerator) generate(ctx context.Context, sess *session.Session, ev
6363

6464
titleSession := session.New(
6565
session.WithUserMessage(userPrompt),
66-
session.WithTitle("Generating title..."),
66+
session.WithTitle("Generating title"),
6767
)
6868

6969
titleRuntime, err := New(newTeam, WithSessionCompaction(false))

pkg/tools/builtin/api.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func (t *APITool) Tools(context.Context) ([]tools.Tool, error) {
148148
Handler: t.handler.CallTool,
149149
Annotations: tools.ToolAnnotations{
150150
ReadOnlyHint: true,
151-
Title: "API URLs",
151+
Title: defaultsTo(t.config.Name, "Query API"),
152152
},
153153
},
154154
}, nil
@@ -161,3 +161,10 @@ func (t *APITool) Start(context.Context) error {
161161
func (t *APITool) Stop(context.Context) error {
162162
return nil
163163
}
164+
165+
func defaultsTo(value, defaultValue string) string {
166+
if value != "" {
167+
return value
168+
}
169+
return defaultValue
170+
}

pkg/tools/builtin/filesystem.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ func (t *FilesystemTool) Tools(context.Context) ([]tools.Tool, error) {
217217
OutputSchema: tools.MustSchemaFor[string](),
218218
Handler: NewHandler(t.handleEditFile),
219219
Annotations: tools.ToolAnnotations{
220-
Title: "Edit File",
220+
Title: "Edit",
221221
},
222222
},
223223
{
@@ -263,7 +263,7 @@ func (t *FilesystemTool) Tools(context.Context) ([]tools.Tool, error) {
263263
Handler: NewHandler(t.handleReadFile),
264264
Annotations: tools.ToolAnnotations{
265265
ReadOnlyHint: true,
266-
Title: "Read File",
266+
Title: "Read",
267267
},
268268
},
269269
{
@@ -311,7 +311,7 @@ func (t *FilesystemTool) Tools(context.Context) ([]tools.Tool, error) {
311311
OutputSchema: tools.MustSchemaFor[string](),
312312
Handler: NewHandler(t.handleWriteFile),
313313
Annotations: tools.ToolAnnotations{
314-
Title: "Write File",
314+
Title: "Write",
315315
},
316316
},
317317
}, nil

pkg/tools/builtin/shell.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ func (t *ShellTool) Tools(context.Context) ([]tools.Tool, error) {
581581
OutputSchema: tools.MustSchemaFor[string](),
582582
Handler: NewHandler(t.handler.RunShell),
583583
Annotations: tools.ToolAnnotations{
584-
Title: "Run Shell Command",
584+
Title: "Shell",
585585
},
586586
},
587587
{
@@ -592,7 +592,7 @@ func (t *ShellTool) Tools(context.Context) ([]tools.Tool, error) {
592592
OutputSchema: tools.MustSchemaFor[string](),
593593
Handler: NewHandler(t.handler.RunShellBackground),
594594
Annotations: tools.ToolAnnotations{
595-
Title: "Run Shell Command in Background",
595+
Title: "Background Job",
596596
},
597597
},
598598
{

pkg/tui/commands/commands.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ func BuildCommandCategories(ctx context.Context, application *app.App) []Categor
138138
// Truncate long descriptions to fit on one line
139139
description := prompt
140140
if len(description) > 60 {
141-
description = description[:57] + "..."
141+
description = description[:59] + ""
142142
}
143143

144144
commands = append(commands, Item{
@@ -187,7 +187,7 @@ func BuildCommandCategories(ctx context.Context, application *app.App) []Categor
187187

188188
// Truncate long descriptions to fit on one line
189189
if len(description) > 55 {
190-
description = description[:52] + "..."
190+
description = description[:54] + ""
191191
}
192192

193193
// Create closure variables to capture current iteration values

pkg/tui/components/editor/editor.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ type editor struct {
112112
func New(a *app.App, hist *history.History) Editor {
113113
ta := textarea.New()
114114
ta.SetStyles(styles.InputStyle)
115-
ta.Placeholder = "Type your message here..."
116-
ta.Prompt = ""
115+
ta.Placeholder = "Type your message here"
116+
ta.Prompt = ""
117117
ta.CharLimit = -1
118118
ta.SetWidth(50)
119119
ta.SetHeight(3) // Set minimum 3 lines for multi-line input
@@ -557,7 +557,7 @@ func (e *editor) View() string {
557557
view = lipgloss.JoinVertical(lipgloss.Left, bannerView, view)
558558
}
559559

560-
return styles.EditorStyle.Render(view)
560+
return styles.RenderComposite(styles.TabPrimaryStyle.Padding(0, 1).MarginBottom(1).Width(e.width), styles.EditorStyle.Render(view))
561561
}
562562

563563
// SetSize sets the dimensions of the component

pkg/tui/components/message/message.go

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,24 @@ type Model interface {
2323

2424
// messageModel implements Model
2525
type messageModel struct {
26-
message *types.Message
26+
message *types.Message
27+
previous *types.Message
28+
2729
width int
2830
height int
2931
focused bool
3032
spinner spinner.Spinner
3133
}
3234

3335
// New creates a new message view
34-
func New(msg *types.Message) *messageModel {
36+
func New(msg, previous *types.Message) *messageModel {
3537
return &messageModel{
36-
message: msg,
37-
width: 80, // Default width
38-
height: 1, // Will be calculated
39-
focused: false,
40-
spinner: spinner.New(spinner.ModeBoth),
38+
message: msg,
39+
previous: previous,
40+
width: 80, // Default width
41+
height: 1, // Will be calculated
42+
focused: false,
43+
spinner: spinner.New(spinner.ModeBoth),
4144
}
4245
}
4346

@@ -77,31 +80,39 @@ func (mv *messageModel) Render(width int) string {
7780
case types.MessageTypeSpinner:
7881
return mv.spinner.View()
7982
case types.MessageTypeUser:
80-
return styles.UserMessageBorderStyle.Width(width - 1).Render(msg.Content)
83+
return styles.UserMessageStyle.Width(width - 1).Render(msg.Content)
8184
case types.MessageTypeAssistant:
8285
if msg.Content == "" {
8386
return mv.spinner.View()
8487
}
8588

86-
rendered, err := markdown.NewRenderer(width).Render(msg.Content)
89+
rendered, err := markdown.NewRenderer(width - styles.AssistantMessageStyle.GetPaddingLeft()).Render(msg.Content)
8790
if err != nil {
88-
return senderPrefix(msg.Sender) + msg.Content
91+
rendered = msg.Content
92+
} else {
93+
rendered = strings.TrimRight(rendered, "\n\r\t ")
94+
}
95+
96+
if mv.previous != nil && mv.previous.Type == msg.Type && mv.previous.Sender == msg.Sender {
97+
return styles.AssistantMessageStyle.Render(rendered)
8998
}
9099

91-
return senderPrefix(msg.Sender) + strings.TrimRight(rendered, "\n\r\t ")
100+
return mv.senderPrefix(msg.Sender) + styles.AssistantMessageStyle.Render(rendered)
92101
case types.MessageTypeAssistantReasoning:
93102
if msg.Content == "" {
94103
return mv.spinner.View()
95104
}
96-
// Render through the markdown renderer to ensure proper wrapping to width
105+
97106
rendered, err := markdown.NewRenderer(width).Render(msg.Content)
98107
if err != nil {
99-
text := "Thinking: " + senderPrefix(msg.Sender) + msg.Content
108+
text := "Thinking: " + mv.senderPrefix(msg.Sender) + msg.Content
100109
return styles.MutedStyle.Italic(true).Render(text)
101110
}
111+
102112
// Strip ANSI from inner rendering so muted style fully applies
103113
clean := stripANSI(strings.TrimRight(rendered, "\n\r\t "))
104-
thinkingText := "Thinking: " + senderPrefix(msg.Sender) + clean
114+
thinkingText := "Thinking: " + mv.senderPrefix(msg.Sender) + clean
115+
105116
return styles.MutedStyle.Italic(true).Render(thinkingText)
106117
case types.MessageTypeShellOutput:
107118
if rendered, err := markdown.NewRenderer(width).Render(fmt.Sprintf("```console\n%s\n```", msg.Content)); err == nil {
@@ -111,19 +122,19 @@ func (mv *messageModel) Render(width int) string {
111122
case types.MessageTypeCancelled:
112123
return styles.WarningStyle.Render("⚠ stream cancelled ⚠")
113124
case types.MessageTypeWelcome:
114-
return styles.WelcomeMessageBorderStyle.Width(width - 1).Render(strings.TrimRight(msg.Content, "\n\r\t "))
125+
return styles.WelcomeMessageStyle.Width(width - 1).Render(strings.TrimRight(msg.Content, "\n\r\t "))
115126
case types.MessageTypeError:
116127
return styles.ErrorMessageStyle.Width(width - 1).Render(msg.Content)
117128
default:
118129
return msg.Content
119130
}
120131
}
121132

122-
func senderPrefix(sender string) string {
133+
func (mv *messageModel) senderPrefix(sender string) string {
123134
if sender == "" {
124135
return ""
125136
}
126-
return styles.AgentBadgeStyle.Render("["+sender+"]") + "\n\n"
137+
return styles.AgentBadgeStyle.Render(sender+"") + "\n\n"
127138
}
128139

129140
// Height calculates the height needed for this message view

pkg/tui/components/message/message_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ func TestErrorMessageWrapping(t *testing.T) {
1818
"It contains enough text to exceed typical terminal widths and demonstrate the wrapping behavior."
1919

2020
msg := types.Error(longError)
21-
mv := New(msg)
21+
mv := New(msg, nil)
2222

2323
// Set a narrow width to force wrapping
2424
width := 50
@@ -48,7 +48,7 @@ func TestErrorMessageWithShortContent(t *testing.T) {
4848

4949
shortError := "Short error"
5050
msg := types.Error(shortError)
51-
mv := New(msg)
51+
mv := New(msg, nil)
5252

5353
width := 80
5454
mv.SetSize(width, 0)
@@ -68,7 +68,7 @@ func TestErrorMessagePreservesContent(t *testing.T) {
6868

6969
errorContent := "Error: Failed to connect to database\nConnection timeout after 30 seconds"
7070
msg := types.Error(errorContent)
71-
mv := New(msg)
71+
mv := New(msg, nil)
7272

7373
width := 80
7474
mv.SetSize(width, 0)

pkg/tui/components/messages/messages.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -515,15 +515,17 @@ func (m *model) ensureAllItemsRendered() {
515515

516516
for i, view := range m.views {
517517
item := m.renderItem(i, view)
518+
if item.view == "" {
519+
continue
520+
}
518521

519522
// Add content to complete rendered string
520-
if item.view != "" {
521-
lines := strings.Split(item.view, "\n")
522-
allLines = append(allLines, lines...)
523-
}
523+
view := strings.TrimSuffix(item.view, "\n")
524+
lines := strings.Split(view, "\n")
525+
allLines = append(allLines, lines...)
524526

525527
// Add separator between messages (but not after last message)
526-
if i < len(m.views)-1 && item.view != "" {
528+
if i < len(m.views)-1 {
527529
allLines = append(allLines, "")
528530
}
529531
}
@@ -586,6 +588,7 @@ func (m *model) addMessage(msg *types.Message) tea.Cmd {
586588
m.messages = append(m.messages, msg)
587589

588590
view := m.createMessageView(msg)
591+
m.sessionState.PreviousMessage = msg
589592
m.views = append(m.views, view)
590593

591594
var cmds []tea.Cmd
@@ -715,7 +718,7 @@ func (m *model) createToolCallView(msg *types.Message) layout.Model {
715718
}
716719

717720
func (m *model) createMessageView(msg *types.Message) layout.Model {
718-
view := message.New(msg)
721+
view := message.New(msg, m.sessionState.PreviousMessage)
719722
view.SetSize(m.width, 0)
720723
return view
721724
}

0 commit comments

Comments
 (0)