Skip to content

Commit 6675b6c

Browse files
authored
Merge pull request #1141 from dgageot/tui2
More TUI work
2 parents 8d2b43d + 8595290 commit 6675b6c

File tree

25 files changed

+268
-199
lines changed

25 files changed

+268
-199
lines changed

pkg/tools/builtin/filesystem.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,10 @@ func (t *FilesystemTool) handleReadFile(_ context.Context, args ReadFileArgs) (*
602602

603603
content, err := os.ReadFile(args.Path)
604604
if err != nil {
605-
return tools.ResultError(fmt.Sprintf("Error reading file: %s", err)), nil
605+
if os.IsNotExist(err) {
606+
return tools.ResultError("not found"), nil
607+
}
608+
return tools.ResultError(fmt.Sprintf("%s", err)), nil
606609
}
607610

608611
return tools.ResultSuccess(string(content)), nil
@@ -637,7 +640,10 @@ func (t *FilesystemTool) handleReadMultipleFiles(ctx context.Context, args ReadM
637640

638641
content, err := os.ReadFile(path)
639642
if err != nil {
640-
errMsg := fmt.Sprintf("Error reading file: %s", err)
643+
errMsg := fmt.Sprintf("%s", err)
644+
if os.IsNotExist(err) {
645+
errMsg = "not found"
646+
}
641647
contents = append(contents, PathContent{
642648
Path: path,
643649
Content: errMsg,

pkg/tools/builtin/filesystem_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ func TestFilesystemTool_ReadFile(t *testing.T) {
188188
Path: filepath.Join(tmpDir, "nonexistent.txt"),
189189
})
190190
require.NoError(t, err)
191-
assert.Contains(t, result.Output, "Error reading file")
191+
assert.Equal(t, "not found", result.Output)
192192

193193
result, err = tool.handleReadFile(t.Context(), ReadFileArgs{
194194
Path: "/etc/passwd",
@@ -225,7 +225,7 @@ func TestFilesystemTool_ReadMultipleFiles(t *testing.T) {
225225
})
226226
require.NoError(t, err)
227227
assert.Contains(t, result.Output, content1)
228-
assert.Contains(t, result.Output, "Error reading file")
228+
assert.Contains(t, result.Output, "not found")
229229
}
230230

231231
func TestFilesystemTool_ListDirectory(t *testing.T) {

pkg/tui/components/message/message.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ func (mv *messageModel) senderPrefix(sender string) string {
134134
if sender == "" {
135135
return ""
136136
}
137-
return styles.AgentBadgeStyle.Render(sender+" ▶") + "\n\n"
137+
return styles.AgentBadgeStyle.Render(sender) + "\n\n"
138138
}
139139

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

pkg/tui/components/messages/messages.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -698,10 +698,10 @@ func (m *model) AppendToLastMessage(agentName string, messageType types.MessageT
698698
m.invalidateItem(lastIdx)
699699
// Content will auto-scroll in View() if user hasn't scrolled
700700
return nil
701-
} else {
702-
// Creating a new message, use addMessage for proper auto-scroll
703-
return m.addMessage(types.Agent(messageType, agentName, content))
704701
}
702+
703+
// Creating a new message, use addMessage for proper auto-scroll
704+
return m.addMessage(types.Agent(messageType, agentName, content))
705705
}
706706

707707
// ScrollToBottom scrolls to the bottom of the chat
@@ -729,18 +729,20 @@ func (m *model) createMessageView(msg *types.Message) layout.Model {
729729

730730
// removeSpinner removes the last message if it's a spinner
731731
func (m *model) removeSpinner() {
732-
if len(m.messages) > 0 {
733-
lastIdx := len(m.messages) - 1
734-
lastMessage := m.messages[lastIdx]
735-
736-
if lastMessage.Type == types.MessageTypeSpinner {
737-
m.messages = m.messages[:lastIdx]
738-
if len(m.views) > lastIdx {
739-
m.views = m.views[:lastIdx]
740-
}
741-
// Invalidate all items since we've removed a message
742-
m.invalidateAllItems()
732+
if len(m.messages) == 0 {
733+
return
734+
}
735+
736+
lastIdx := len(m.messages) - 1
737+
lastMessage := m.messages[lastIdx]
738+
739+
if lastMessage.Type == types.MessageTypeSpinner {
740+
m.messages = m.messages[:lastIdx]
741+
if len(m.views) > lastIdx {
742+
m.views = m.views[:lastIdx]
743743
}
744+
// Invalidate all items since we've removed a message
745+
m.invalidateAllItems()
744746
}
745747
}
746748

pkg/tui/components/sidebar/sidebar.go

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ type Model interface {
3434

3535
SetTokenUsage(event *runtime.TokenUsageEvent)
3636
SetTodos(result *tools.ToolCallResult) error
37-
SetWorking(working bool) tea.Cmd
3837
SetMode(mode Mode)
3938
SetAgentInfo(agentName, model, description string)
4039
SetTeamInfo(availableAgents []string)
@@ -57,7 +56,6 @@ type model struct {
5756
sessionUsage map[string]*runtime.Usage // sessionID -> latest usage snapshot
5857
sessionAgent map[string]string // sessionID -> agent name
5958
todoComp *todotool.SidebarComponent
60-
working bool
6159
mcpInit bool
6260
ragIndexing map[string]*ragIndexingState // strategy name -> indexing state
6361
spinner spinner.Spinner
@@ -103,15 +101,6 @@ func (m *model) SetTodos(result *tools.ToolCallResult) error {
103101
return m.todoComp.SetTodos(result)
104102
}
105103

106-
// SetWorking sets the working state and returns a command to start the spinner if needed
107-
func (m *model) SetWorking(working bool) tea.Cmd {
108-
m.working = working
109-
if working {
110-
return m.spinner.Init()
111-
}
112-
return nil
113-
}
114-
115104
// SetAgentInfo sets the current agent information
116105
func (m *model) SetAgentInfo(agentName, model, description string) {
117106
m.currentAgent = agentName
@@ -232,20 +221,16 @@ func (m *model) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
232221
default:
233222
var cmds []tea.Cmd
234223

235-
// Update main spinner for working/mcpInit states
236-
if m.working || m.mcpInit {
237-
var cmd tea.Cmd
238-
var model layout.Model
239-
model, cmd = m.spinner.Update(msg)
224+
// Update main spinner
225+
if m.mcpInit {
226+
model, cmd := m.spinner.Update(msg)
240227
m.spinner = model.(spinner.Spinner)
241228
cmds = append(cmds, cmd)
242229
}
243230

244231
// Update each RAG indexing spinner
245232
for _, state := range m.ragIndexing {
246-
var cmd tea.Cmd
247-
var model layout.Model
248-
model, cmd = state.spinner.Update(msg)
233+
model, cmd := state.spinner.Update(msg)
249234
state.spinner = model.(spinner.Spinner)
250235
cmds = append(cmds, cmd)
251236
}
@@ -286,8 +271,6 @@ func (m *model) verticalView() string {
286271
}
287272
if working := m.workingIndicator(); working != "" {
288273
session = append(session, working)
289-
} else {
290-
session = append(session, "") // spacer for layout consistency
291274
}
292275

293276
var main []string
@@ -314,11 +297,6 @@ func (m *model) verticalView() string {
314297
func (m *model) workingIndicator() string {
315298
var indicators []string
316299

317-
// Add working indicator if agent is processing
318-
if m.working {
319-
indicators = append(indicators, styles.ActiveStyle.Render(m.spinner.View()+" "+"Working…"))
320-
}
321-
322300
// Add MCP init indicator if initializing
323301
if m.mcpInit {
324302
indicators = append(indicators, styles.ActiveStyle.Render(m.spinner.View()+" "+"Initializing MCP servers…"))
@@ -395,10 +373,6 @@ func (m *model) workingIndicator() string {
395373
func (m *model) workingIndicatorHorizontal() string {
396374
var labels []string
397375

398-
// Add working indicator if agent is processing
399-
if m.working {
400-
labels = append(labels, "Working…")
401-
}
402376
// Add MCP init indicator if initializing
403377
if m.mcpInit {
404378
labels = append(labels, "Initializing MCP servers…")
@@ -534,9 +508,8 @@ func (m *model) agentInfo() string {
534508

535509
// Agent description if available
536510
if m.agentDescription != "" {
537-
// Truncate description for sidebar display
538511
description := m.agentDescription
539-
maxDescWidth := max(m.width-4, 20) // Leave margin for styling
512+
maxDescWidth := m.width - 2
540513
if len(description) > maxDescWidth {
541514
description = description[:maxDescWidth-1] + "…"
542515
}

pkg/tui/components/tab/tab.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ func Render(title, content string, width int) string {
1111

1212
b.WriteString(styles.RenderComposite(styles.TabTitleStyle, title+" "+strings.Repeat("─", width-len(title)-1)))
1313
b.WriteString("\n")
14-
b.WriteString(styles.RenderComposite(styles.TabStyle.Width(width-2), content))
14+
b.WriteString(styles.RenderComposite(styles.TabStyle.Width(width), content))
1515
b.WriteString("\n")
1616

1717
return b.String()
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package allowed
2+
3+
import (
4+
"encoding/json"
5+
6+
tea "charm.land/bubbletea/v2"
7+
8+
"github.com/docker/cagent/pkg/tools/builtin"
9+
"github.com/docker/cagent/pkg/tui/components/spinner"
10+
"github.com/docker/cagent/pkg/tui/components/toolcommon"
11+
"github.com/docker/cagent/pkg/tui/core/layout"
12+
"github.com/docker/cagent/pkg/tui/service"
13+
"github.com/docker/cagent/pkg/tui/types"
14+
)
15+
16+
type Component struct {
17+
message *types.Message
18+
spinner spinner.Spinner
19+
width int
20+
height int
21+
}
22+
23+
func New(msg *types.Message, _ *service.SessionState) layout.Model {
24+
return &Component{
25+
message: msg,
26+
spinner: spinner.New(spinner.ModeSpinnerOnly),
27+
width: 80,
28+
height: 1,
29+
}
30+
}
31+
32+
func (c *Component) SetSize(width, height int) tea.Cmd {
33+
c.width = width
34+
c.height = height
35+
return nil
36+
}
37+
38+
func (c *Component) Init() tea.Cmd {
39+
if c.message.ToolStatus == types.ToolStatusPending || c.message.ToolStatus == types.ToolStatusRunning {
40+
return c.spinner.Init()
41+
}
42+
return nil
43+
}
44+
45+
func (c *Component) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
46+
if c.message.ToolStatus == types.ToolStatusPending || c.message.ToolStatus == types.ToolStatusRunning {
47+
model, cmd := c.spinner.Update(msg)
48+
c.spinner = model.(spinner.Spinner)
49+
return c, cmd
50+
}
51+
52+
return c, nil
53+
}
54+
55+
func (c *Component) View() string {
56+
msg := c.message
57+
58+
var args builtin.AddAllowedDirectoryArgs
59+
if err := json.Unmarshal([]byte(msg.ToolCall.Function.Arguments), &args); err != nil {
60+
return toolcommon.RenderTool(msg, c.spinner, "", "", c.width)
61+
}
62+
63+
return toolcommon.RenderTool(msg, c.spinner, args.Path, "", c.width)
64+
}

pkg/tui/components/tool/api/apitool.go

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,7 @@ func (c *Component) Init() tea.Cmd {
4949

5050
func (c *Component) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
5151
if c.message.ToolStatus == types.ToolStatusPending || c.message.ToolStatus == types.ToolStatusRunning {
52-
var cmd tea.Cmd
53-
var model layout.Model
54-
model, cmd = c.spinner.Update(msg)
52+
model, cmd := c.spinner.Update(msg)
5553
c.spinner = model.(spinner.Spinner)
5654
return c, cmd
5755
}
@@ -64,12 +62,9 @@ func (c *Component) View() string {
6462

6563
var args map[string]any
6664
if err := json.Unmarshal([]byte(msg.ToolCall.Function.Arguments), &args); err != nil {
67-
return toolcommon.RenderTool(msg, c.spinner, msg.ToolDefinition.DisplayName(), "", c.width)
65+
return toolcommon.RenderTool(msg, c.spinner, "", "", c.width)
6866
}
6967

70-
// Build the display name with inline result
71-
displayName := msg.ToolDefinition.DisplayName()
72-
7368
// Extract argument summary for the tool call display
7469
var params string
7570
if argsText := formatArgs(args); argsText != "" {
@@ -91,7 +86,7 @@ func (c *Component) View() string {
9186
}
9287

9388
// Render everything on one line
94-
return toolcommon.RenderTool(msg, c.spinner, displayName+" "+params, "", c.width)
89+
return toolcommon.RenderTool(msg, c.spinner, params, "", c.width)
9590
}
9691

9792
// extractEndpoint tries to find the endpoint/URL being called

pkg/tui/components/tool/defaulttool/defaulttool.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,7 @@ func (c *Component) Init() tea.Cmd {
4848

4949
func (c *Component) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
5050
if c.message.ToolStatus == types.ToolStatusPending || c.message.ToolStatus == types.ToolStatusRunning {
51-
var cmd tea.Cmd
52-
var model layout.Model
53-
model, cmd = c.spinner.Update(msg)
51+
model, cmd := c.spinner.Update(msg)
5452
c.spinner = model.(spinner.Spinner)
5553
return c, cmd
5654
}
@@ -60,21 +58,21 @@ func (c *Component) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
6058

6159
func (c *Component) View() string {
6260
msg := c.message
63-
displayName := msg.ToolDefinition.DisplayName()
6461

6562
var argsContent string
6663
if msg.ToolCall.Function.Arguments != "" {
64+
displayName := msg.ToolDefinition.DisplayName()
6765
argsContent = renderToolArgs(msg.ToolCall, c.width-4-len(displayName), c.width-3)
6866
}
6967

7068
if argsContent == "" {
71-
return toolcommon.RenderTool(msg, c.spinner, msg.ToolDefinition.DisplayName(), "", c.width)
69+
return toolcommon.RenderTool(msg, c.spinner, "", "", c.width)
7270
}
7371

7472
var resultContent string
7573
if (msg.ToolStatus == types.ToolStatusCompleted || msg.ToolStatus == types.ToolStatusError) && msg.Content != "" {
7674
resultContent = toolcommon.FormatToolResult(msg.Content, c.width)
7775
}
7876

79-
return toolcommon.RenderTool(msg, c.spinner, displayName+argsContent, resultContent, c.width)
77+
return toolcommon.RenderTool(msg, c.spinner, argsContent, resultContent, c.width)
8078
}

pkg/tui/components/tool/defaulttool/render.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func renderToolArgs(toolCall tools.ToolCall, shortWidth, width int) string {
4848
}
4949

5050
if len(short.String()) <= shortWidth && !strings.Contains(short.String(), "\n") {
51-
return " " + short.String()
51+
return short.String()
5252
}
5353

5454
return "\n" + styles.ToolCallArgs.Width(width).Render(strings.TrimSuffix(md.String(), "\n"))

0 commit comments

Comments
 (0)