From 1570105188ebe49195417c66fb0cf680b3799269 Mon Sep 17 00:00:00 2001 From: David Gageot Date: Thu, 18 Dec 2025 14:06:29 +0100 Subject: [PATCH] New TUI Signed-off-by: David Gageot --- pkg/runtime/title_generator.go | 2 +- pkg/tools/builtin/api.go | 9 +- pkg/tools/builtin/filesystem.go | 6 +- pkg/tools/builtin/shell.go | 4 +- pkg/tui/commands/commands.go | 4 +- pkg/tui/components/editor/editor.go | 6 +- pkg/tui/components/message/message.go | 45 ++-- pkg/tui/components/message/message_test.go | 6 +- pkg/tui/components/messages/messages.go | 15 +- pkg/tui/components/sidebar/sidebar.go | 102 ++++----- pkg/tui/components/statusbar/statusbar.go | 4 +- pkg/tui/components/tab/tab.go | 18 ++ .../{webtool/webtool.go => api/apitool.go} | 27 +-- .../tool/defaulttool/defaulttool.go | 7 +- pkg/tui/components/tool/defaulttool/render.go | 94 ++++---- pkg/tui/components/tool/editfile/editfile.go | 7 +- pkg/tui/components/tool/factory.go | 6 +- pkg/tui/components/tool/handoff/handoff.go | 2 +- .../tool/listdirectory/listdirectory.go | 7 +- pkg/tui/components/tool/readfile/readfile.go | 6 +- .../readmultiplefiles/readmultiplefiles.go | 25 ++- .../tool/searchfiles/searchfiles.go | 7 +- pkg/tui/components/tool/shell/shell.go | 6 +- pkg/tui/components/tool/todotool/component.go | 37 ++-- pkg/tui/components/tool/todotool/sidebar.go | 29 ++- pkg/tui/components/tool/todotool/todotool.go | 21 +- .../tool/transfertask/transfertask.go | 8 +- .../components/tool/writefile/writefile.go | 6 +- pkg/tui/components/toolcommon/common.go | 26 ++- pkg/tui/dialog/command_palette.go | 2 +- pkg/tui/page/chat/chat.go | 68 +++--- pkg/tui/service/sessionstate.go | 5 +- pkg/tui/styles/styles.go | 203 +++++++++--------- pkg/tui/tui.go | 4 +- 34 files changed, 408 insertions(+), 416 deletions(-) create mode 100644 pkg/tui/components/tab/tab.go rename pkg/tui/components/tool/{webtool/webtool.go => api/apitool.go} (82%) diff --git a/pkg/runtime/title_generator.go b/pkg/runtime/title_generator.go index ede3902b3..b0583fab0 100644 --- a/pkg/runtime/title_generator.go +++ b/pkg/runtime/title_generator.go @@ -63,7 +63,7 @@ func (t *titleGenerator) generate(ctx context.Context, sess *session.Session, ev titleSession := session.New( session.WithUserMessage(userPrompt), - session.WithTitle("Generating title..."), + session.WithTitle("Generating title…"), ) titleRuntime, err := New(newTeam, WithSessionCompaction(false)) diff --git a/pkg/tools/builtin/api.go b/pkg/tools/builtin/api.go index 83d04b53f..aff969a4f 100644 --- a/pkg/tools/builtin/api.go +++ b/pkg/tools/builtin/api.go @@ -148,7 +148,7 @@ func (t *APITool) Tools(context.Context) ([]tools.Tool, error) { Handler: t.handler.CallTool, Annotations: tools.ToolAnnotations{ ReadOnlyHint: true, - Title: "API URLs", + Title: defaultsTo(t.config.Name, "Query API"), }, }, }, nil @@ -161,3 +161,10 @@ func (t *APITool) Start(context.Context) error { func (t *APITool) Stop(context.Context) error { return nil } + +func defaultsTo(value, defaultValue string) string { + if value != "" { + return value + } + return defaultValue +} diff --git a/pkg/tools/builtin/filesystem.go b/pkg/tools/builtin/filesystem.go index 6bd09c4c4..216bb305f 100644 --- a/pkg/tools/builtin/filesystem.go +++ b/pkg/tools/builtin/filesystem.go @@ -217,7 +217,7 @@ func (t *FilesystemTool) Tools(context.Context) ([]tools.Tool, error) { OutputSchema: tools.MustSchemaFor[string](), Handler: NewHandler(t.handleEditFile), Annotations: tools.ToolAnnotations{ - Title: "Edit File", + Title: "Edit", }, }, { @@ -263,7 +263,7 @@ func (t *FilesystemTool) Tools(context.Context) ([]tools.Tool, error) { Handler: NewHandler(t.handleReadFile), Annotations: tools.ToolAnnotations{ ReadOnlyHint: true, - Title: "Read File", + Title: "Read", }, }, { @@ -311,7 +311,7 @@ func (t *FilesystemTool) Tools(context.Context) ([]tools.Tool, error) { OutputSchema: tools.MustSchemaFor[string](), Handler: NewHandler(t.handleWriteFile), Annotations: tools.ToolAnnotations{ - Title: "Write File", + Title: "Write", }, }, }, nil diff --git a/pkg/tools/builtin/shell.go b/pkg/tools/builtin/shell.go index f67c499e5..b84e5ded1 100644 --- a/pkg/tools/builtin/shell.go +++ b/pkg/tools/builtin/shell.go @@ -581,7 +581,7 @@ func (t *ShellTool) Tools(context.Context) ([]tools.Tool, error) { OutputSchema: tools.MustSchemaFor[string](), Handler: NewHandler(t.handler.RunShell), Annotations: tools.ToolAnnotations{ - Title: "Run Shell Command", + Title: "Shell", }, }, { @@ -592,7 +592,7 @@ func (t *ShellTool) Tools(context.Context) ([]tools.Tool, error) { OutputSchema: tools.MustSchemaFor[string](), Handler: NewHandler(t.handler.RunShellBackground), Annotations: tools.ToolAnnotations{ - Title: "Run Shell Command in Background", + Title: "Background Job", }, }, { diff --git a/pkg/tui/commands/commands.go b/pkg/tui/commands/commands.go index 78543a0ee..bf074c13f 100644 --- a/pkg/tui/commands/commands.go +++ b/pkg/tui/commands/commands.go @@ -138,7 +138,7 @@ func BuildCommandCategories(ctx context.Context, application *app.App) []Categor // Truncate long descriptions to fit on one line description := prompt if len(description) > 60 { - description = description[:57] + "..." + description = description[:59] + "…" } commands = append(commands, Item{ @@ -187,7 +187,7 @@ func BuildCommandCategories(ctx context.Context, application *app.App) []Categor // Truncate long descriptions to fit on one line if len(description) > 55 { - description = description[:52] + "..." + description = description[:54] + "…" } // Create closure variables to capture current iteration values diff --git a/pkg/tui/components/editor/editor.go b/pkg/tui/components/editor/editor.go index 250c23e0d..16bc757d8 100644 --- a/pkg/tui/components/editor/editor.go +++ b/pkg/tui/components/editor/editor.go @@ -112,8 +112,8 @@ type editor struct { func New(a *app.App, hist *history.History) Editor { ta := textarea.New() ta.SetStyles(styles.InputStyle) - ta.Placeholder = "Type your message here..." - ta.Prompt = "│ " + ta.Placeholder = "Type your message here…" + ta.Prompt = "" ta.CharLimit = -1 ta.SetWidth(50) ta.SetHeight(3) // Set minimum 3 lines for multi-line input @@ -557,7 +557,7 @@ func (e *editor) View() string { view = lipgloss.JoinVertical(lipgloss.Left, bannerView, view) } - return styles.EditorStyle.Render(view) + return styles.RenderComposite(styles.TabPrimaryStyle.Padding(0, 1).MarginBottom(1).Width(e.width), styles.EditorStyle.Render(view)) } // SetSize sets the dimensions of the component diff --git a/pkg/tui/components/message/message.go b/pkg/tui/components/message/message.go index 5dd3c7b45..97a55956c 100644 --- a/pkg/tui/components/message/message.go +++ b/pkg/tui/components/message/message.go @@ -23,7 +23,9 @@ type Model interface { // messageModel implements Model type messageModel struct { - message *types.Message + message *types.Message + previous *types.Message + width int height int focused bool @@ -31,13 +33,14 @@ type messageModel struct { } // New creates a new message view -func New(msg *types.Message) *messageModel { +func New(msg, previous *types.Message) *messageModel { return &messageModel{ - message: msg, - width: 80, // Default width - height: 1, // Will be calculated - focused: false, - spinner: spinner.New(spinner.ModeBoth), + message: msg, + previous: previous, + width: 80, // Default width + height: 1, // Will be calculated + focused: false, + spinner: spinner.New(spinner.ModeBoth), } } @@ -77,31 +80,39 @@ func (mv *messageModel) Render(width int) string { case types.MessageTypeSpinner: return mv.spinner.View() case types.MessageTypeUser: - return styles.UserMessageBorderStyle.Width(width - 1).Render(msg.Content) + return styles.UserMessageStyle.Width(width - 1).Render(msg.Content) case types.MessageTypeAssistant: if msg.Content == "" { return mv.spinner.View() } - rendered, err := markdown.NewRenderer(width).Render(msg.Content) + rendered, err := markdown.NewRenderer(width - styles.AssistantMessageStyle.GetPaddingLeft()).Render(msg.Content) if err != nil { - return senderPrefix(msg.Sender) + msg.Content + rendered = msg.Content + } else { + rendered = strings.TrimRight(rendered, "\n\r\t ") + } + + if mv.previous != nil && mv.previous.Type == msg.Type && mv.previous.Sender == msg.Sender { + return styles.AssistantMessageStyle.Render(rendered) } - return senderPrefix(msg.Sender) + strings.TrimRight(rendered, "\n\r\t ") + return mv.senderPrefix(msg.Sender) + styles.AssistantMessageStyle.Render(rendered) case types.MessageTypeAssistantReasoning: if msg.Content == "" { return mv.spinner.View() } - // Render through the markdown renderer to ensure proper wrapping to width + rendered, err := markdown.NewRenderer(width).Render(msg.Content) if err != nil { - text := "Thinking: " + senderPrefix(msg.Sender) + msg.Content + text := "Thinking: " + mv.senderPrefix(msg.Sender) + msg.Content return styles.MutedStyle.Italic(true).Render(text) } + // Strip ANSI from inner rendering so muted style fully applies clean := stripANSI(strings.TrimRight(rendered, "\n\r\t ")) - thinkingText := "Thinking: " + senderPrefix(msg.Sender) + clean + thinkingText := "Thinking: " + mv.senderPrefix(msg.Sender) + clean + return styles.MutedStyle.Italic(true).Render(thinkingText) case types.MessageTypeShellOutput: if rendered, err := markdown.NewRenderer(width).Render(fmt.Sprintf("```console\n%s\n```", msg.Content)); err == nil { @@ -111,7 +122,7 @@ func (mv *messageModel) Render(width int) string { case types.MessageTypeCancelled: return styles.WarningStyle.Render("⚠ stream cancelled ⚠") case types.MessageTypeWelcome: - return styles.WelcomeMessageBorderStyle.Width(width - 1).Render(strings.TrimRight(msg.Content, "\n\r\t ")) + return styles.WelcomeMessageStyle.Width(width - 1).Render(strings.TrimRight(msg.Content, "\n\r\t ")) case types.MessageTypeError: return styles.ErrorMessageStyle.Width(width - 1).Render(msg.Content) default: @@ -119,11 +130,11 @@ func (mv *messageModel) Render(width int) string { } } -func senderPrefix(sender string) string { +func (mv *messageModel) senderPrefix(sender string) string { if sender == "" { return "" } - return styles.AgentBadgeStyle.Render("["+sender+"]") + "\n\n" + return styles.AgentBadgeStyle.Render(sender+" ▶") + "\n\n" } // Height calculates the height needed for this message view diff --git a/pkg/tui/components/message/message_test.go b/pkg/tui/components/message/message_test.go index ee7125dec..6e59bd27d 100644 --- a/pkg/tui/components/message/message_test.go +++ b/pkg/tui/components/message/message_test.go @@ -18,7 +18,7 @@ func TestErrorMessageWrapping(t *testing.T) { "It contains enough text to exceed typical terminal widths and demonstrate the wrapping behavior." msg := types.Error(longError) - mv := New(msg) + mv := New(msg, nil) // Set a narrow width to force wrapping width := 50 @@ -48,7 +48,7 @@ func TestErrorMessageWithShortContent(t *testing.T) { shortError := "Short error" msg := types.Error(shortError) - mv := New(msg) + mv := New(msg, nil) width := 80 mv.SetSize(width, 0) @@ -68,7 +68,7 @@ func TestErrorMessagePreservesContent(t *testing.T) { errorContent := "Error: Failed to connect to database\nConnection timeout after 30 seconds" msg := types.Error(errorContent) - mv := New(msg) + mv := New(msg, nil) width := 80 mv.SetSize(width, 0) diff --git a/pkg/tui/components/messages/messages.go b/pkg/tui/components/messages/messages.go index be72b6351..411448713 100644 --- a/pkg/tui/components/messages/messages.go +++ b/pkg/tui/components/messages/messages.go @@ -515,15 +515,17 @@ func (m *model) ensureAllItemsRendered() { for i, view := range m.views { item := m.renderItem(i, view) + if item.view == "" { + continue + } // Add content to complete rendered string - if item.view != "" { - lines := strings.Split(item.view, "\n") - allLines = append(allLines, lines...) - } + view := strings.TrimSuffix(item.view, "\n") + lines := strings.Split(view, "\n") + allLines = append(allLines, lines...) // Add separator between messages (but not after last message) - if i < len(m.views)-1 && item.view != "" { + if i < len(m.views)-1 { allLines = append(allLines, "") } } @@ -586,6 +588,7 @@ func (m *model) addMessage(msg *types.Message) tea.Cmd { m.messages = append(m.messages, msg) view := m.createMessageView(msg) + m.sessionState.PreviousMessage = msg m.views = append(m.views, view) var cmds []tea.Cmd @@ -715,7 +718,7 @@ func (m *model) createToolCallView(msg *types.Message) layout.Model { } func (m *model) createMessageView(msg *types.Message) layout.Model { - view := message.New(msg) + view := message.New(msg, m.sessionState.PreviousMessage) view.SetSize(m.width, 0) return view } diff --git a/pkg/tui/components/sidebar/sidebar.go b/pkg/tui/components/sidebar/sidebar.go index b146e7c55..f83171f39 100644 --- a/pkg/tui/components/sidebar/sidebar.go +++ b/pkg/tui/components/sidebar/sidebar.go @@ -14,6 +14,7 @@ import ( "github.com/docker/cagent/pkg/runtime" "github.com/docker/cagent/pkg/tools" "github.com/docker/cagent/pkg/tui/components/spinner" + "github.com/docker/cagent/pkg/tui/components/tab" "github.com/docker/cagent/pkg/tui/components/tool/todotool" "github.com/docker/cagent/pkg/tui/core/layout" "github.com/docker/cagent/pkg/tui/styles" @@ -155,7 +156,7 @@ func (m *model) contextPercent() (string, bool) { for _, usage := range m.sessionUsage { if usage.ContextLimit > 0 { percent := (float64(usage.ContextLength) / float64(usage.ContextLimit)) * 100 - return fmt.Sprintf("Context: %.0f%%", percent), true + return fmt.Sprintf("%.0f%%", percent), true } } return "", false @@ -278,39 +279,34 @@ func (m *model) horizontalView() string { } func (m *model) verticalView() string { - var main []string - main = append(main, m.sessionTitle) + var session []string + session = append(session, m.sessionTitle) if pwd := getCurrentWorkingDirectory(); pwd != "" { - main = append(main, styles.MutedStyle.Render(pwd)) + session = append(session, "", styles.TabAccentStyle.Render(pwd)) } if working := m.workingIndicator(); working != "" { - main = append(main, working) - } else { - main = append(main, "") // spacer for layout consistency + session = append(session, working) } - var more []string + var main []string + main = append(main, m.renderTab("Session", strings.Join(session, "\n"))) if agentInfo := m.agentInfo(); agentInfo != "" { - more = append(more, agentInfo) + main = append(main, agentInfo) } if toolsetInfo := m.toolsetInfo(); toolsetInfo != "" { - more = append(more, toolsetInfo) + main = append(main, toolsetInfo) } if usage := m.tokenUsage(); usage != "" { - more = append(more, usage) + main = append(main, usage) } m.todoComp.SetSize(m.width) todoContent := strings.TrimSuffix(m.todoComp.Render(), "\n") if todoContent != "" { - more = append(more, todoContent) + main = append(main, todoContent) } - return styles.BaseStyle. - Width(m.width). - Height(m.height-2). - Align(lipgloss.Left, lipgloss.Top). - Render(strings.Join(main, "\n") + "\n\n" + strings.Join(more, "\n\n")) + return strings.Join(main, "\n\n") } func (m *model) workingIndicator() string { @@ -318,12 +314,12 @@ func (m *model) workingIndicator() string { // Add working indicator if agent is processing if m.working { - indicators = append(indicators, styles.ActiveStyle.Render(m.spinner.View()+" "+"Working...")) + indicators = append(indicators, styles.ActiveStyle.Render(m.spinner.View()+" "+"Working…")) } // Add MCP init indicator if initializing if m.mcpInit { - indicators = append(indicators, styles.ActiveStyle.Render(m.spinner.View()+" "+"Initializing MCP servers...")) + indicators = append(indicators, styles.ActiveStyle.Render(m.spinner.View()+" "+"Initializing MCP servers…")) } // Add RAG indexing indicators for each active indexing operation @@ -399,12 +395,12 @@ func (m *model) workingIndicatorHorizontal() string { // Add working indicator if agent is processing if m.working { - labels = append(labels, "Working...") + labels = append(labels, "Working…") } // Add MCP init indicator if initializing if m.mcpInit { - labels = append(labels, "Initializing MCP servers...") + labels = append(labels, "Initializing MCP servers…") } // Add RAG indexing labels for each active indexing operation @@ -484,18 +480,14 @@ func (m *model) tokenUsage() string { totalCost += usage.Cost } - var b strings.Builder - b.WriteString(styles.HighlightStyle.Render("Usage")) - b.WriteString("\n") - b.WriteString(styles.MutedStyle.Render(fmt.Sprintf("Tokens: %s", formatTokenCount(totalTokens)))) - b.WriteString("\n") - b.WriteString(styles.MutedStyle.Render(fmt.Sprintf("Cost: $%s", formatCost(totalCost)))) + var tokenUsage strings.Builder + fmt.Fprintf(&tokenUsage, "%s", formatTokenCount(totalTokens)) if ctxText, ok := m.contextPercent(); ok { - b.WriteString("\n") - b.WriteString(styles.MutedStyle.Render(ctxText)) + fmt.Fprintf(&tokenUsage, " (%s)", ctxText) } + fmt.Fprintf(&tokenUsage, " %s", styles.TabAccentStyle.Render("$"+formatCost(totalCost))) - return b.String() + return m.renderTab("Token Usage", tokenUsage.String()) } // tokenUsageSummary returns a single-line summary for horizontal layout. @@ -512,7 +504,7 @@ func (m *model) tokenUsageSummary() string { } if ctxText, ok := m.contextPercent(); ok { - return fmt.Sprintf("Tokens: %s | Cost: $%s | %s", formatTokenCount(totalTokens), formatCost(totalCost), ctxText) + return fmt.Sprintf("Tokens: %s | Cost: $%s | Context: %s", formatTokenCount(totalTokens), formatCost(totalCost), ctxText) } return fmt.Sprintf("Tokens: %s | Cost: $%s", formatTokenCount(totalTokens), formatCost(totalCost)) @@ -524,51 +516,46 @@ func (m *model) agentInfo() string { return "" } - var content strings.Builder - // Agent name with highlight and switching indicator agentTitle := "Agent" if m.agentSwitching { agentTitle += " ↔" // switching indicator } - content.WriteString(styles.HighlightStyle.Render(agentTitle)) - content.WriteString("\n") // Current agent name agentName := m.currentAgent if m.agentSwitching { agentName = "⟳ " + agentName // switching icon } - content.WriteString(styles.MutedStyle.Render(agentName)) - - // Team info if multiple agents available - if len(m.availableAgents) > 1 { - content.WriteString("\n") - teamInfo := fmt.Sprintf("Team: %d agents", len(m.availableAgents)) - content.WriteString(styles.SubtleStyle.Render(teamInfo)) - } - // Model info if available - if m.agentModel != "" { - content.WriteString("\n") - content.WriteString(styles.SubtleStyle.Render("Model: " + m.agentModel)) - } + var content strings.Builder + content.WriteString(styles.TabAccentStyle.Render(agentName)) // Agent description if available if m.agentDescription != "" { - content.WriteString("\n") - // Truncate description for sidebar display description := m.agentDescription maxDescWidth := max(m.width-4, 20) // Leave margin for styling if len(description) > maxDescWidth { - description = description[:maxDescWidth-3] + "..." + description = description[:maxDescWidth-1] + "…" } - content.WriteString(styles.SubtleStyle.Render(description)) + fmt.Fprintf(&content, "\n%s", description) + } + + // Team info if multiple agents available + if len(m.availableAgents) > 1 { + fmt.Fprintf(&content, "\nTeam: %d agents", len(m.availableAgents)) + } + + // Model info if available + if m.agentModel != "" { + provider, model, _ := strings.Cut(m.agentModel, "/") + fmt.Fprintf(&content, "\nProvider: %s", provider) + fmt.Fprintf(&content, "\nModel: %s", model) } - return content.String() + return m.renderTab(agentTitle, content.String()) } // toolsetInfo renders the current toolset status information @@ -577,10 +564,7 @@ func (m *model) toolsetInfo() string { return "" } - var content strings.Builder - content.WriteString(styles.HighlightStyle.Render("Tools")) - content.WriteString(styles.MutedStyle.Render(fmt.Sprintf("\n%d tools available", m.availableTools))) - return content.String() + return m.renderTab("Tools", fmt.Sprintf("%d tools available", m.availableTools)) } // SetSize sets the dimensions of the component @@ -599,3 +583,7 @@ func (m *model) GetSize() (width, height int) { func (m *model) SetMode(mode Mode) { m.mode = mode } + +func (m *model) renderTab(title, content string) string { + return tab.Render(title, content, m.width-2) +} diff --git a/pkg/tui/components/statusbar/statusbar.go b/pkg/tui/components/statusbar/statusbar.go index bd6e6b512..1c0928146 100644 --- a/pkg/tui/components/statusbar/statusbar.go +++ b/pkg/tui/components/statusbar/statusbar.go @@ -34,8 +34,8 @@ func (s *StatusBar) formatHelpString(bindings []key.Binding) string { var helpParts []string for _, binding := range bindings { if binding.Help().Key != "" && binding.Help().Desc != "" { - keyPart := styles.StatusStyle.Render(binding.Help().Key) - actionPart := styles.ActionStyle.Render(binding.Help().Desc) + keyPart := styles.HighlightWhiteStyle.Render(binding.Help().Key) + actionPart := styles.SecondaryStyle.Render(binding.Help().Desc) helpParts = append(helpParts, keyPart+" "+actionPart) } } diff --git a/pkg/tui/components/tab/tab.go b/pkg/tui/components/tab/tab.go new file mode 100644 index 000000000..538589dc3 --- /dev/null +++ b/pkg/tui/components/tab/tab.go @@ -0,0 +1,18 @@ +package tab + +import ( + "strings" + + "github.com/docker/cagent/pkg/tui/styles" +) + +func Render(title, content string, width int) string { + var b strings.Builder + + b.WriteString(styles.RenderComposite(styles.TabTitleStyle, title+" "+strings.Repeat("─", width-len(title)-1))) + b.WriteString("\n") + b.WriteString(styles.RenderComposite(styles.TabStyle.Width(width-2), content)) + b.WriteString("\n") + + return b.String() +} diff --git a/pkg/tui/components/tool/webtool/webtool.go b/pkg/tui/components/tool/api/apitool.go similarity index 82% rename from pkg/tui/components/tool/webtool/webtool.go rename to pkg/tui/components/tool/api/apitool.go index dbffd4aa3..b0f4247eb 100644 --- a/pkg/tui/components/tool/webtool/webtool.go +++ b/pkg/tui/components/tool/api/apitool.go @@ -1,4 +1,4 @@ -package webtool +package api import ( "encoding/json" @@ -62,25 +62,18 @@ func (c *Component) Update(msg tea.Msg) (layout.Model, tea.Cmd) { func (c *Component) View() string { msg := c.message - // Parse the arguments to extract info about the API call var args map[string]any - var progressText string - if err := json.Unmarshal([]byte(msg.ToolCall.Function.Arguments), &args); err != nil { - // If we can't parse, show spinner while running - if msg.ToolStatus == types.ToolStatusRunning { - progressText = c.spinner.View() - } - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), msg.ToolDefinition.DisplayName(), progressText, "", c.width) + return toolcommon.RenderTool(msg, c.spinner, msg.ToolDefinition.DisplayName(), "", c.width) } - // Extract argument summary for the tool call display - argsText := formatArgs(args) - // Build the display name with inline result displayName := msg.ToolDefinition.DisplayName() - if argsText != "" { - displayName = displayName + "(" + styles.MutedStyle.Render(argsText) + ")" + + // Extract argument summary for the tool call display + var params string + if argsText := formatArgs(args); argsText != "" { + params = "(" + argsText + ")" } // Add inline result/progress after the tool name @@ -89,16 +82,16 @@ func (c *Component) View() string { // While running, show what we're calling endpoint := extractEndpoint(args) if endpoint != "" { - displayName += styles.MutedStyle.Render(": Calling " + endpoint) + params += styles.MutedStyle.Render(": Calling " + endpoint) } case types.ToolStatusCompleted: // When completed, show a brief summary inline resultSummary := extractSummary(msg.Content) - displayName += styles.MutedStyle.Render(": " + resultSummary) + params += styles.MutedStyle.Render(": " + resultSummary) } // Render everything on one line - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), displayName, "", "", c.width) + return toolcommon.RenderTool(msg, c.spinner, displayName+" "+params, "", c.width) } // extractEndpoint tries to find the endpoint/URL being called diff --git a/pkg/tui/components/tool/defaulttool/defaulttool.go b/pkg/tui/components/tool/defaulttool/defaulttool.go index b1928ec39..22c30e444 100644 --- a/pkg/tui/components/tool/defaulttool/defaulttool.go +++ b/pkg/tui/components/tool/defaulttool/defaulttool.go @@ -7,7 +7,6 @@ import ( "github.com/docker/cagent/pkg/tui/components/toolcommon" "github.com/docker/cagent/pkg/tui/core/layout" "github.com/docker/cagent/pkg/tui/service" - "github.com/docker/cagent/pkg/tui/styles" "github.com/docker/cagent/pkg/tui/types" ) @@ -65,11 +64,11 @@ func (c *Component) View() string { var argsContent string if msg.ToolCall.Function.Arguments != "" { - argsContent = renderToolArgs(msg.ToolCall, c.width-3) + argsContent = renderToolArgs(msg.ToolCall, c.width-4-len(displayName), c.width-3) } if argsContent == "" { - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), msg.ToolDefinition.DisplayName(), c.spinner.View(), "", c.width) + return toolcommon.RenderTool(msg, c.spinner, msg.ToolDefinition.DisplayName(), "", c.width) } var resultContent string @@ -77,5 +76,5 @@ func (c *Component) View() string { resultContent = toolcommon.FormatToolResult(msg.Content, c.width) } - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), styles.HighlightStyle.Render(displayName), argsContent, resultContent, c.width) + return toolcommon.RenderTool(msg, c.spinner, displayName+argsContent, resultContent, c.width) } diff --git a/pkg/tui/components/tool/defaulttool/render.go b/pkg/tui/components/tool/defaulttool/render.go index 0b763b1c1..15449907b 100644 --- a/pkg/tui/components/tool/defaulttool/render.go +++ b/pkg/tui/components/tool/defaulttool/render.go @@ -9,67 +9,83 @@ import ( "github.com/docker/cagent/pkg/tui/styles" ) -func renderToolArgs(toolCall tools.ToolCall, width int) string { - decoder := json.NewDecoder(strings.NewReader(toolCall.Function.Arguments)) +type kv struct { + Key string + Value any +} - tok, err := decoder.Token() +func renderToolArgs(toolCall tools.ToolCall, shortWidth, width int) string { + args, err := decodeArguments(toolCall.Function.Arguments) if err != nil { return "" } - if delim, ok := tok.(json.Delim); !ok || delim != '{' { - return "" - } - - type kv struct { - Key string - Value any - } - var kvs []kv - - for decoder.More() { - tok, err := decoder.Token() - if err != nil { - return "" - } - key, ok := tok.(string) - if !ok { - return "" - } - - var val any - if err := decoder.Decode(&val); err != nil { - return "" - } - - kvs = append(kvs, kv{Key: key, Value: val}) - } - _, _ = decoder.Token() - - style := styles.ToolCallArgs.Width(width) + var short strings.Builder var md strings.Builder - for i, kv := range kvs { + for i, arg := range args { if i > 0 { + short.WriteString(" ") md.WriteString("\n") } var content string - if v, ok := kv.Value.(string); ok { + if v, ok := arg.Value.(string); ok { content = v } else { - buf, err := json.MarshalIndent(kv.Value, "", " ") + buf, err := json.MarshalIndent(arg.Value, "", " ") if err != nil { - content = fmt.Sprintf("%v", kv.Value) + content = fmt.Sprintf("%v", arg.Value) } else { content = string(buf) } } - fmt.Fprintf(&md, "%s:\n%s", styles.ToolCallArgKey.Render(kv.Key), content) + fmt.Fprintf(&short, "%s=%s", arg.Key, content) + fmt.Fprintf(&md, "%s:\n%s", arg.Key, content) if !strings.HasSuffix(content, "\n") { md.WriteString("\n") } } - return "\n" + style.Render(strings.TrimSuffix(md.String(), "\n")) + if len(short.String()) <= shortWidth && !strings.Contains(short.String(), "\n") { + return " " + short.String() + } + + return "\n" + styles.ToolCallArgs.Width(width).Render(strings.TrimSuffix(md.String(), "\n")) +} + +// decodeArguments decodes the JSON-encoded arguments string into an ordered slice of key-value pairs. +func decodeArguments(arguments string) ([]kv, error) { + decoder := json.NewDecoder(strings.NewReader(arguments)) + + tok, err := decoder.Token() + if err != nil { + return nil, err + } + if delim, ok := tok.(json.Delim); !ok || delim != '{' { + return nil, err + } + + var args []kv + + for decoder.More() { + tok, err := decoder.Token() + if err != nil { + return nil, err + } + key, ok := tok.(string) + if !ok { + return nil, err + } + + var val any + if err := decoder.Decode(&val); err != nil { + return nil, err + } + + args = append(args, kv{Key: key, Value: val}) + } + _, _ = decoder.Token() + + return args, nil } diff --git a/pkg/tui/components/tool/editfile/editfile.go b/pkg/tui/components/tool/editfile/editfile.go index 5ac76659b..8b41d25d4 100644 --- a/pkg/tui/components/tool/editfile/editfile.go +++ b/pkg/tui/components/tool/editfile/editfile.go @@ -66,17 +66,14 @@ func (c *Component) Update(msg tea.Msg) (layout.Model, tea.Cmd) { func (c *Component) View() string { msg := c.message + var args builtin.EditFileArgs if err := json.Unmarshal([]byte(msg.ToolCall.Function.Arguments), &args); err != nil { return "" } displayName := msg.ToolDefinition.DisplayName() - content := fmt.Sprintf("%s %s %s", toolcommon.Icon(msg.ToolStatus), styles.HighlightStyle.Render(displayName), styles.MutedStyle.Render(args.Path)) - - if msg.ToolStatus == types.ToolStatusPending || msg.ToolStatus == types.ToolStatusRunning { - content += " " + c.spinner.View() - } + content := fmt.Sprintf("%s %s %s", toolcommon.Icon(msg, c.spinner), styles.ToolMessageStyle.Render(displayName), styles.ToolMessageStyle.Render(args.Path)) if msg.ToolCall.Function.Arguments != "" { content += "\n\n" + styles.ToolCallResult.Render(renderEditFile(msg.ToolCall, c.width-1, c.sessionState.SplitDiffView, msg.ToolStatus)) diff --git a/pkg/tui/components/tool/factory.go b/pkg/tui/components/tool/factory.go index d9c3e81b5..768e68eb6 100644 --- a/pkg/tui/components/tool/factory.go +++ b/pkg/tui/components/tool/factory.go @@ -2,6 +2,7 @@ package tool import ( "github.com/docker/cagent/pkg/tools/builtin" + "github.com/docker/cagent/pkg/tui/components/tool/api" "github.com/docker/cagent/pkg/tui/components/tool/defaulttool" "github.com/docker/cagent/pkg/tui/components/tool/editfile" "github.com/docker/cagent/pkg/tui/components/tool/handoff" @@ -12,7 +13,6 @@ import ( "github.com/docker/cagent/pkg/tui/components/tool/shell" "github.com/docker/cagent/pkg/tui/components/tool/todotool" "github.com/docker/cagent/pkg/tui/components/tool/transfertask" - "github.com/docker/cagent/pkg/tui/components/tool/webtool" "github.com/docker/cagent/pkg/tui/components/tool/writefile" "github.com/docker/cagent/pkg/tui/core/layout" "github.com/docker/cagent/pkg/tui/service" @@ -71,10 +71,10 @@ func newDefaultRegistry() *Registry { registry.Register(builtin.ToolNameUpdateTodo, todotool.New) registry.Register(builtin.ToolNameListTodos, todotool.New) registry.Register(builtin.ToolNameShell, shell.New) + registry.Register(builtin.ToolNameFetch, api.New) // Register category-based handlers - registry.Register("category:api", webtool.New) - registry.Register(builtin.ToolNameFetch, webtool.New) + registry.Register("category:api", api.New) return registry } diff --git a/pkg/tui/components/tool/handoff/handoff.go b/pkg/tui/components/tool/handoff/handoff.go index 22f1c70ec..5f6a93fca 100644 --- a/pkg/tui/components/tool/handoff/handoff.go +++ b/pkg/tui/components/tool/handoff/handoff.go @@ -44,5 +44,5 @@ func (c *Component) View() string { return "" // TODO: Partial tool call } - return styles.AgentBadgeStyle.Render("["+c.message.Sender+"]") + styles.MutedStyle.Render("hands off to") + styles.AgentBadgeStyle.Render("["+params.Agent+"]") + return styles.AgentBadgeStyle.Render(c.message.Sender) + " ─► " + styles.AgentBadgeStyle.Render(params.Agent+" ▶") } diff --git a/pkg/tui/components/tool/listdirectory/listdirectory.go b/pkg/tui/components/tool/listdirectory/listdirectory.go index af79b019f..b5bd68a81 100644 --- a/pkg/tui/components/tool/listdirectory/listdirectory.go +++ b/pkg/tui/components/tool/listdirectory/listdirectory.go @@ -66,10 +66,9 @@ func (c *Component) Update(msg tea.Msg) (layout.Model, tea.Cmd) { func (c *Component) View() string { msg := c.message - // Parse arguments var args builtin.ListDirectoryArgs if err := json.Unmarshal([]byte(msg.ToolCall.Function.Arguments), &args); err != nil { - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), msg.ToolDefinition.DisplayName(), c.spinner.View(), "", c.width) + return toolcommon.RenderTool(msg, c.spinner, msg.ToolDefinition.DisplayName(), "", c.width) } // Shorten the path for display @@ -78,7 +77,7 @@ func (c *Component) View() string { // For pending/running state, show spinner if msg.ToolStatus == types.ToolStatusPending || msg.ToolStatus == types.ToolStatusRunning { - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), displayName, c.spinner.View(), "", c.width) + return toolcommon.RenderTool(msg, c.spinner, displayName, "", c.width) } // For completed/error state, show concise summary @@ -91,7 +90,7 @@ func (c *Component) View() string { summary := formatSummary(meta) params := styles.MutedStyle.Render(summary) - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), displayName, params, "", c.width) + return toolcommon.RenderTool(msg, c.spinner, displayName+" "+params, "", c.width) } // formatSummary creates a concise summary of the directory listing from metadata diff --git a/pkg/tui/components/tool/readfile/readfile.go b/pkg/tui/components/tool/readfile/readfile.go index 3a575f51d..cfdfd2e58 100644 --- a/pkg/tui/components/tool/readfile/readfile.go +++ b/pkg/tui/components/tool/readfile/readfile.go @@ -10,7 +10,6 @@ import ( "github.com/docker/cagent/pkg/tui/components/toolcommon" "github.com/docker/cagent/pkg/tui/core/layout" "github.com/docker/cagent/pkg/tui/service" - "github.com/docker/cagent/pkg/tui/styles" "github.com/docker/cagent/pkg/tui/types" ) @@ -62,10 +61,11 @@ func (c *Component) Update(msg tea.Msg) (layout.Model, tea.Cmd) { func (c *Component) View() string { msg := c.message + var args builtin.ReadFileArgs if err := json.Unmarshal([]byte(msg.ToolCall.Function.Arguments), &args); err != nil { - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), msg.ToolDefinition.DisplayName(), c.spinner.View(), "", c.width) + return toolcommon.RenderTool(msg, c.spinner, msg.ToolDefinition.DisplayName(), "", c.width) } - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), msg.ToolDefinition.DisplayName(), styles.MutedStyle.Render(args.Path), "", c.width) + return toolcommon.RenderTool(msg, c.spinner, msg.ToolDefinition.DisplayName()+" "+args.Path, "", c.width) } diff --git a/pkg/tui/components/tool/readmultiplefiles/readmultiplefiles.go b/pkg/tui/components/tool/readmultiplefiles/readmultiplefiles.go index 32e0f26af..b2de3c18a 100644 --- a/pkg/tui/components/tool/readmultiplefiles/readmultiplefiles.go +++ b/pkg/tui/components/tool/readmultiplefiles/readmultiplefiles.go @@ -69,13 +69,13 @@ func (c *Component) View() string { // Parse arguments var args builtin.ReadMultipleFilesArgs if err := json.Unmarshal([]byte(msg.ToolCall.Function.Arguments), &args); err != nil { - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), "Read Multiple Files", c.spinner.View(), "", c.width) + return toolcommon.RenderTool(msg, c.spinner, "Read Multiple Files", "", c.width) } // For pending/running state, show files being read if msg.ToolStatus == types.ToolStatusPending || msg.ToolStatus == types.ToolStatusRunning { params := formatFilesList(args.Paths) - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), "Read Multiple Files", params, c.spinner.View(), c.width) + return toolcommon.RenderTool(msg, c.spinner, "Read Multiple Files "+params, "", c.width) } // For completed/error state, render header line followed by each file line @@ -85,18 +85,19 @@ func (c *Component) View() string { meta = &m } } - summaries := formatSummaryLines(meta) // Build output with header and separate lines for each file var content strings.Builder // Header line - icon := toolcommon.Icon(msg.ToolStatus) - content.WriteString(fmt.Sprintf("%s %s:\n", icon, styles.HighlightStyle.Render("Read Multiple Files"))) + icon := toolcommon.Icon(msg, c.spinner) // Each file on its own line with checkmark - for _, summary := range summaries { - content.WriteString(fmt.Sprintf("%s %s: %s\n", icon, summary.displayName, summary.params)) + for _, summary := range formatSummaryLines(meta) { + if content.Len() > 0 { + content.WriteString("\n") + } + fmt.Fprintf(&content, "%s %s %s", icon, styles.ToolMessageStyle.Render(summary.displayName), summary.params) } // Apply tool message styling @@ -118,15 +119,13 @@ func formatSummaryLines(meta *builtin.ReadMultipleFilesMeta) []fileSummary { summaries := make([]fileSummary, 0, len(meta.Files)) for _, file := range meta.Files { - shortPath := shortenPath(file.Path, homeDir) - displayName := fmt.Sprintf("Read(%s)", shortPath) - var params string + params := shortenPath(file.Path, homeDir) if file.Error != "" { - params = file.Error + params += " " + file.Error } else { - params = fmt.Sprintf("Read %d lines", file.LineCount) + params += fmt.Sprintf(" %d lines", file.LineCount) } - summaries = append(summaries, fileSummary{displayName: displayName, params: params}) + summaries = append(summaries, fileSummary{displayName: "Read", params: params}) } return summaries diff --git a/pkg/tui/components/tool/searchfiles/searchfiles.go b/pkg/tui/components/tool/searchfiles/searchfiles.go index f07731c91..74100e28b 100644 --- a/pkg/tui/components/tool/searchfiles/searchfiles.go +++ b/pkg/tui/components/tool/searchfiles/searchfiles.go @@ -67,7 +67,7 @@ func (c *Component) View() string { // Parse arguments var args builtin.SearchFilesArgs if err := json.Unmarshal([]byte(msg.ToolCall.Function.Arguments), &args); err != nil { - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), msg.ToolDefinition.DisplayName(), c.spinner.View(), "", c.width) + return toolcommon.RenderTool(msg, c.spinner, msg.ToolDefinition.DisplayName(), "", c.width) } // Format display name with pattern @@ -75,14 +75,13 @@ func (c *Component) View() string { // For pending/running state, show spinner if msg.ToolStatus == types.ToolStatusPending || msg.ToolStatus == types.ToolStatusRunning { - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), displayName, c.spinner.View(), "", c.width) + return toolcommon.RenderTool(msg, c.spinner, displayName, "", c.width) } // For completed/error state, show concise summary summary := formatSummary(msg.Content) - params := fmt.Sprintf(": %s", summary) - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), displayName, params, "", c.width) + return toolcommon.RenderTool(msg, c.spinner, displayName+" "+summary, "", c.width) } // formatSummary creates a concise summary of the search results diff --git a/pkg/tui/components/tool/shell/shell.go b/pkg/tui/components/tool/shell/shell.go index 8665e0232..142d3df80 100644 --- a/pkg/tui/components/tool/shell/shell.go +++ b/pkg/tui/components/tool/shell/shell.go @@ -10,7 +10,6 @@ import ( "github.com/docker/cagent/pkg/tui/components/toolcommon" "github.com/docker/cagent/pkg/tui/core/layout" "github.com/docker/cagent/pkg/tui/service" - "github.com/docker/cagent/pkg/tui/styles" "github.com/docker/cagent/pkg/tui/types" ) @@ -60,10 +59,11 @@ func (c *Component) Update(msg tea.Msg) (layout.Model, tea.Cmd) { func (c *Component) View() string { msg := c.message + var args builtin.RunShellArgs if err := json.Unmarshal([]byte(msg.ToolCall.Function.Arguments), &args); err != nil { - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), msg.ToolDefinition.DisplayName(), c.spinner.View(), "", c.width) + return toolcommon.RenderTool(msg, c.spinner, msg.ToolDefinition.DisplayName(), "", c.width) } - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), msg.ToolDefinition.DisplayName(), styles.MutedStyle.Render(args.Cmd), "", c.width) + return toolcommon.RenderTool(msg, c.spinner, msg.ToolDefinition.DisplayName()+" "+args.Cmd, "", c.width) } diff --git a/pkg/tui/components/tool/todotool/component.go b/pkg/tui/components/tool/todotool/component.go index 1b1f08d12..acacc912e 100644 --- a/pkg/tui/components/tool/todotool/component.go +++ b/pkg/tui/components/tool/todotool/component.go @@ -2,6 +2,7 @@ package todotool import ( "fmt" + "strings" tea "charm.land/bubbletea/v2" @@ -68,8 +69,10 @@ func (c *Component) View() string { // Render based on tool type switch toolName { - case builtin.ToolNameCreateTodo, builtin.ToolNameCreateTodos, builtin.ToolNameUpdateTodo: + case builtin.ToolNameCreateTodo, builtin.ToolNameCreateTodos: return c.renderTodos() + case builtin.ToolNameUpdateTodo: + return "" // We've got todos in the sidebar case builtin.ToolNameListTodos: return c.renderList() default: @@ -80,44 +83,28 @@ func (c *Component) View() string { func (c *Component) renderTodos() string { msg := c.message displayName := msg.ToolDefinition.DisplayName() - content := fmt.Sprintf("%s %s", toolcommon.Icon(msg.ToolStatus), styles.HighlightStyle.Render(displayName)) - if msg.ToolStatus == types.ToolStatusPending || msg.ToolStatus == types.ToolStatusRunning { - content += " " + c.spinner.View() - } + var content strings.Builder + fmt.Fprintf(&content, "%s %s", toolcommon.Icon(msg, c.spinner), styles.ToolMessageStyle.Render(displayName)) - if msg.ToolResult != nil && msg.ToolResult.Meta != nil { - if todos, ok := msg.ToolResult.Meta.([]builtin.Todo); ok { - for _, todo := range todos { - icon, style := renderTodoIcon(todo.Status) - todoLine := fmt.Sprintf("\n%s %s", style.Render(icon), style.Render(todo.Description)) - content += todoLine - } - } - } - - return styles.RenderComposite(styles.ToolMessageStyle.Width(c.width-1), content) + return styles.RenderComposite(styles.ToolMessageStyle.Width(c.width-1), content.String()) } func (c *Component) renderList() string { msg := c.message displayName := msg.ToolDefinition.DisplayName() - content := fmt.Sprintf("%s %s", toolcommon.Icon(msg.ToolStatus), styles.HighlightStyle.Render(displayName)) - - if msg.ToolStatus == types.ToolStatusPending || msg.ToolStatus == types.ToolStatusRunning { - content += " " + c.spinner.View() - } + var content strings.Builder + content.WriteString(fmt.Sprintf("%s %s", toolcommon.Icon(msg, c.spinner), styles.ToolMessageStyle.Render(displayName))) if msg.ToolResult != nil && msg.ToolResult.Meta != nil { if todos, ok := msg.ToolResult.Meta.([]builtin.Todo); ok { for _, todo := range todos { icon, style := renderTodoIcon(todo.Status) - descStyle := renderTodoDescriptionStyle(todo.Status) - todoLine := fmt.Sprintf("\n%s %s", style.Render(icon), descStyle.Render(todo.Description)) - content += todoLine + todoLine := fmt.Sprintf("\n%s %s", style.Render(icon), style.Render(todo.Description)) + content.WriteString(todoLine) } } } - return styles.RenderComposite(styles.ToolMessageStyle.Width(c.width-1), content) + return styles.RenderComposite(styles.ToolMessageStyle.Width(c.width-1), content.String()) } diff --git a/pkg/tui/components/tool/todotool/sidebar.go b/pkg/tui/components/tool/todotool/sidebar.go index 00d4d73b9..d68586e10 100644 --- a/pkg/tui/components/tool/todotool/sidebar.go +++ b/pkg/tui/components/tool/todotool/sidebar.go @@ -1,11 +1,11 @@ package todotool import ( - "fmt" "strings" "github.com/docker/cagent/pkg/tools" "github.com/docker/cagent/pkg/tools/builtin" + "github.com/docker/cagent/pkg/tui/components/tab" "github.com/docker/cagent/pkg/tui/styles" ) @@ -44,29 +44,26 @@ func (c *SidebarComponent) Render() string { return "" } - var content strings.Builder - content.WriteString(styles.HighlightStyle.Render("Todo")) - content.WriteString("\n") - + var lines []string for _, todo := range c.todos { - content.WriteString(renderTodoLine(todo, c.width)) - content.WriteString("\n") + lines = append(lines, c.renderTodoLine(todo)) } - return content.String() + return c.renderTab("TO-DO", strings.Join(lines, "\n")) } -func renderTodoLine(todo builtin.Todo, maxWidth int) string { - icon, iconStyle := renderTodoIcon(todo.Status) - descStyle := renderTodoDescriptionStyle(todo.Status) +func (c *SidebarComponent) renderTodoLine(todo builtin.Todo) string { + icon, style := renderTodoIcon(todo.Status) description := todo.Description - maxDescWidth := max(maxWidth-2, 3) + maxDescWidth := max(c.width-6, 3) if len(description) > maxDescWidth { - description = description[:maxDescWidth-3] + "..." + description = description[:maxDescWidth-1] + "…" } - styledIcon := iconStyle.Render(icon) - styledDescription := descStyle.Render(description) - return fmt.Sprintf("%s %s", styledIcon, styledDescription) + return styles.TabPrimaryStyle.Render(style.Render(icon + " " + description)) +} + +func (c *SidebarComponent) renderTab(title, content string) string { + return tab.Render(title, content, c.width-2) } diff --git a/pkg/tui/components/tool/todotool/todotool.go b/pkg/tui/components/tool/todotool/todotool.go index 042296eee..321f6427b 100644 --- a/pkg/tui/components/tool/todotool/todotool.go +++ b/pkg/tui/components/tool/todotool/todotool.go @@ -9,25 +9,12 @@ import ( func renderTodoIcon(status string) (string, lipgloss.Style) { switch status { case "pending": - return "◯", styles.PendingStyle + return "◯", styles.ToBeDoneStyle case "in-progress": - return "◕", styles.InProgressStyle + return "◔", styles.InProgressStyle case "completed": - return "✓", styles.MutedStyle + return "✓", styles.CompletedStyle.Strikethrough(true) default: - return "?", styles.BaseStyle - } -} - -func renderTodoDescriptionStyle(status string) lipgloss.Style { - switch status { - case "pending": - return styles.PendingStyle - case "in-progress": - return styles.InProgressStyle - case "completed": - return styles.MutedStyle.Strikethrough(true) - default: - return styles.BaseStyle + return "?", styles.ToBeDoneStyle } } diff --git a/pkg/tui/components/tool/transfertask/transfertask.go b/pkg/tui/components/tool/transfertask/transfertask.go index 9e45da2a5..605722179 100644 --- a/pkg/tui/components/tool/transfertask/transfertask.go +++ b/pkg/tui/components/tool/transfertask/transfertask.go @@ -44,7 +44,9 @@ func (c *Component) View() string { return "" // TODO: Partial tool call } - badge := styles.AgentBadgeStyle.Render("["+c.message.Sender+"]") + styles.MutedStyle.Render("transfers task to") + styles.AgentBadgeStyle.Render("["+params.Agent+"]"+":") - content := styles.MutedStyle.Render(params.Task) - return badge + "\n\n" + content + return styles.AgentBadgeStyle.Render(c.message.Sender) + + " calls " + + styles.AgentBadgeStyle.Render(params.Agent+" ▶") + + "\n\n" + + styles.ToolMessageStyle.Render("✓ "+params.Task) } diff --git a/pkg/tui/components/tool/writefile/writefile.go b/pkg/tui/components/tool/writefile/writefile.go index 4a0bc06ae..6f2ae080e 100644 --- a/pkg/tui/components/tool/writefile/writefile.go +++ b/pkg/tui/components/tool/writefile/writefile.go @@ -10,7 +10,6 @@ import ( "github.com/docker/cagent/pkg/tui/components/toolcommon" "github.com/docker/cagent/pkg/tui/core/layout" "github.com/docker/cagent/pkg/tui/service" - "github.com/docker/cagent/pkg/tui/styles" "github.com/docker/cagent/pkg/tui/types" ) @@ -61,10 +60,11 @@ func (c *Component) Update(msg tea.Msg) (layout.Model, tea.Cmd) { func (c *Component) View() string { msg := c.message + var args builtin.WriteFileArgs if err := json.Unmarshal([]byte(msg.ToolCall.Function.Arguments), &args); err != nil { - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), msg.ToolDefinition.DisplayName(), c.spinner.View(), "", c.width) + return toolcommon.RenderTool(msg, c.spinner, msg.ToolDefinition.DisplayName(), "", c.width) } - return toolcommon.RenderTool(toolcommon.Icon(msg.ToolStatus), msg.ToolDefinition.DisplayName(), styles.MutedStyle.Render(args.Path), "", c.width) + return toolcommon.RenderTool(msg, c.spinner, msg.ToolDefinition.DisplayName()+" "+args.Path, "", c.width) } diff --git a/pkg/tui/components/toolcommon/common.go b/pkg/tui/components/toolcommon/common.go index 63f89fb5a..564606022 100644 --- a/pkg/tui/components/toolcommon/common.go +++ b/pkg/tui/components/toolcommon/common.go @@ -5,18 +5,23 @@ import ( "fmt" "strings" + "github.com/docker/cagent/pkg/tui/components/spinner" "github.com/docker/cagent/pkg/tui/styles" "github.com/docker/cagent/pkg/tui/types" ) -func Icon(status types.ToolStatus) string { - switch status { +func Icon(msg *types.Message, inProgress spinner.Spinner) string { + if msg.ToolStatus == types.ToolStatusPending || msg.ToolStatus == types.ToolStatusRunning { + return inProgress.View() + } + + switch msg.ToolStatus { case types.ToolStatusPending: return "⊙" case types.ToolStatusRunning: return "⚙" case types.ToolStatusCompleted: - return styles.SuccessStyle.Render("✓") + return "✓" case types.ToolStatusError: return styles.ErrorStyle.Render("✗") case types.ToolStatusConfirmation: @@ -42,25 +47,26 @@ func FormatToolResult(content string, width int) string { lines := wrapLines(formattedContent, availableWidth) - header := "output" if len(lines) > 10 { lines = lines[:10] - header = "output (truncated)" - lines = append(lines, wrapLines("...", availableWidth)...) + lines = append(lines, wrapLines("…", availableWidth)...) } trimmedContent := strings.Join(lines, "\n") if trimmedContent != "" { - return styles.ToolCallResult.Render(styles.ToolCallResultKey.Render("\n-> "+header+":") + "\n" + trimmedContent) + return styles.ToolCallResult.Render(styles.ToolCallResultKey.Render("\n-> output:") + "\n" + trimmedContent) } return "" } -func RenderTool(icon, name, params, result string, width int) string { - content := fmt.Sprintf("%s %s %s", icon, styles.HighlightStyle.Render(name), styles.MutedStyle.Render(params)) +func RenderTool(msg *types.Message, inProgress spinner.Spinner, name, result string, width int) string { + content := fmt.Sprintf("%s %s", Icon(msg, inProgress), name) if result != "" { - content += "\n" + result + if strings.Count(name, "\n") > 0 { + content += "\n" + } + content += result } return styles.RenderComposite(styles.ToolMessageStyle.Width(width-1), content) } diff --git a/pkg/tui/dialog/command_palette.go b/pkg/tui/dialog/command_palette.go index 95d725e7c..cdb431979 100644 --- a/pkg/tui/dialog/command_palette.go +++ b/pkg/tui/dialog/command_palette.go @@ -62,7 +62,7 @@ func defaultCommandPaletteKeyMap() commandPaletteKeyMap { // NewCommandPaletteDialog creates a new command palette dialog func NewCommandPaletteDialog(categories []commands.Category) Dialog { ti := textinput.New() - ti.Placeholder = "Type to search commands..." + ti.Placeholder = "Type to search commands…" ti.Focus() ti.CharLimit = 100 ti.SetWidth(50) diff --git a/pkg/tui/page/chat/chat.go b/pkg/tui/page/chat/chat.go index bb44d5b01..119c01e04 100644 --- a/pkg/tui/page/chat/chat.go +++ b/pkg/tui/page/chat/chat.go @@ -110,7 +110,7 @@ func defaultKeyMap() KeyMap { return KeyMap{ Tab: key.NewBinding( key.WithKeys("tab"), - key.WithHelp("tab", "switch focus"), + key.WithHelp("TAB", "switch focus"), ), Cancel: key.NewBinding( key.WithKeys("esc"), @@ -119,11 +119,11 @@ func defaultKeyMap() KeyMap { // Ctrl+J acts as a fallback on terminals that don't distinguish Shift+Enter. ShiftNewline: key.NewBinding( key.WithKeys("shift+enter", "ctrl+j"), - key.WithHelp("shift+enter / ctrl+j", "newline"), + key.WithHelp("Shift+Enter / Ctrl+j", "newline"), ), ExternalEditor: key.NewBinding( key.WithKeys("ctrl+g"), - key.WithHelp("ctrl+g", "edit in $EDITOR"), + key.WithHelp("Ctrl+g", "edit in $EDITOR"), ), } } @@ -511,7 +511,7 @@ func (p *chatPage) SetSize(width, height int) tea.Cmd { // Account for horizontal padding in width innerWidth := width - 2 // subtract left/right padding - targetEditorHeight := p.editorLines + targetEditorHeight := p.editorLines - 1 editorCmd := p.editor.SetSize(innerWidth, targetEditorHeight) cmds = append(cmds, editorCmd) @@ -523,11 +523,8 @@ func (p *chatPage) SetSize(width, height int) tea.Cmd { var mainWidth int if width >= minWindowWidth { - mainWidth = innerWidth - sidebarWidth - if mainWidth < 1 { - mainWidth = 1 - } - p.chatHeight = max(1, height-actualEditorHeight-1) // -1 for resize handle + mainWidth = max(innerWidth-sidebarWidth, 1) + p.chatHeight = max(1, height-actualEditorHeight-2) // -1 for resize handle, -1 for empty line before status bar p.sidebar.SetMode(sidebar.ModeVertical) cmds = append(cmds, p.sidebar.SetSize(sidebarWidth, p.chatHeight), @@ -535,11 +532,8 @@ func (p *chatPage) SetSize(width, height int) tea.Cmd { ) } else { const horizontalSidebarHeight = 3 - mainWidth = innerWidth - if mainWidth < 1 { - mainWidth = 1 - } - p.chatHeight = max(1, height-actualEditorHeight-horizontalSidebarHeight-1) // -1 for resize handle + mainWidth = max(innerWidth, 1) + p.chatHeight = max(1, height-actualEditorHeight-horizontalSidebarHeight-2) // -1 for resize handle, -1 for empty line before status bar p.sidebar.SetMode(sidebar.ModeHorizontal) cmds = append(cmds, p.sidebar.SetSize(width, horizontalSidebarHeight), @@ -570,13 +564,15 @@ func (p *chatPage) Bindings() []key.Binding { bindings := []key.Binding{ p.keyMap.Tab, p.keyMap.Cancel, - // show newline hints in the global footer - p.keyMap.ShiftNewline, - p.keyMap.ExternalEditor, } if p.focusedPanel == PanelChat { bindings = append(bindings, p.messages.Bindings()...) + } else { + bindings = append(bindings, + p.keyMap.ShiftNewline, + p.keyMap.ExternalEditor, + ) } return bindings @@ -610,7 +606,7 @@ func (p *chatPage) updateNewlineHelp() { // When keyboard enhancements are supported, show both options p.keyMap.ShiftNewline = key.NewBinding( key.WithKeys("shift+enter", "ctrl+j"), - key.WithHelp("shift+enter / ctrl+j", "newline"), + key.WithHelp("Shift+Enter", "newline"), ) } else { // When not supported, only ctrl+j works @@ -725,7 +721,7 @@ func (p *chatPage) routeMouseEvent(msg tea.Msg, y int) tea.Cmd { func (p *chatPage) isOnResizeLine(y int) bool { // Use current editor height (includes dynamic banner) rather than cached value _, editorHeight := p.editor.GetSize() - return y == p.height-editorHeight-1 + return y == p.height-editorHeight-2 } // isOnResizeHandle checks if (x, y) is on the draggable center of the resize handle. @@ -753,26 +749,22 @@ func (p *chatPage) handleResize(y int) tea.Cmd { // renderResizeHandle renders the draggable separator between messages and editor. func (p *chatPage) renderResizeHandle(width int) string { - if p.isHoveringHandle || p.isDragging { - // Show a small centered highlight when hovered or dragging - handleWidth := min(resizeHandleWidth, width) - sideWidth := (width - handleWidth) / 2 - leftPart := strings.Repeat("─", sideWidth) - centerPart := strings.Repeat("─", handleWidth) - rightPart := strings.Repeat("─", width-sideWidth-handleWidth) - - // Use brighter style when actively dragging - centerStyle := styles.ResizeHandleHoverStyle - if p.isDragging { - centerStyle = styles.ResizeHandleActiveStyle - } - - return styles.ResizeHandleStyle.Render(leftPart) + - centerStyle.Render(centerPart) + - styles.ResizeHandleStyle.Render(rightPart) + // Show a small centered highlight when hovered or dragging + handleWidth := min(resizeHandleWidth, width) + sideWidth := (width - handleWidth) / 2 + leftPart := strings.Repeat("─", sideWidth) + centerPart := strings.Repeat("─", handleWidth) + rightPart := strings.Repeat("─", width-sideWidth-handleWidth-2) + + // Use brighter style when actively dragging + centerStyle := styles.ResizeHandleHoverStyle + if p.isDragging { + centerStyle = styles.ResizeHandleActiveStyle } - // Simple thin line when not hovered - return styles.ResizeHandleStyle.Render(strings.Repeat("─", width)) + + return styles.ResizeHandleStyle.Render(leftPart) + + centerStyle.Render(centerPart) + + styles.ResizeHandleStyle.Render(rightPart) } func (p *chatPage) openAttachmentPreview(preview editor.AttachmentPreview) tea.Cmd { diff --git a/pkg/tui/service/sessionstate.go b/pkg/tui/service/sessionstate.go index e488da506..adfce62ab 100644 --- a/pkg/tui/service/sessionstate.go +++ b/pkg/tui/service/sessionstate.go @@ -1,12 +1,15 @@ package service +import "github.com/docker/cagent/pkg/tui/types" + // SessionState holds shared state across the TUI application. // This provides a centralized location for state that needs to be // accessible by multiple components. type SessionState struct { // SplitDiffView determines whether diff views should be shown side-by-side (true) // or unified (false) - SplitDiffView bool + SplitDiffView bool + PreviousMessage *types.Message } // NewSessionState creates a new SessionState with default values. diff --git a/pkg/tui/styles/styles.go b/pkg/tui/styles/styles.go index 8c5fbca81..83f54ea57 100644 --- a/pkg/tui/styles/styles.go +++ b/pkg/tui/styles/styles.go @@ -17,45 +17,50 @@ const ( // Color hex values (used throughout the file) const ( // Primary colors - ColorAccentBlue = "#7AA2F7" // Soft blue - ColorMutedBlue = "#8B95C1" // Dark blue-grey - ColorBackgroundAlt = "#24283B" // Slightly lighter background - ColorBorderSecondary = "#6B75A8" // Dark blue-grey - ColorTextPrimary = "#C0CAF5" // Light blue-white - ColorTextSecondary = "#9AA5CE" // Medium blue-grey - ColorSuccessGreen = "#9ECE6A" // Soft green - ColorErrorRed = "#F7768E" // Soft red - ColorWarningYellow = "#E0AF68" // Soft yellow + ColorWhite = "#E5F2FC" + ColorAccentBlue = "#7AA2F7" + ColorMutedBlue = "#8B95C1" + ColorMutedGray = "#808080" + ColorBackgroundAlt = "#24283B" + ColorBorderSecondary = "#6B75A8" + ColorTextPrimary = "#C0C0C0" + ColorTextSecondary = "#808080" + ColorSuccessGreen = "#9ECE6A" + ColorErrorRed = "#F7768E" + ColorWarningYellow = "#E0AF68" // Spinner glow colors (transition from base blue towards white) - ColorSpinnerDim = "#9AB8F9" // Lighter blue - ColorSpinnerBright = "#B8CFFB" // Much lighter blue - ColorSpinnerBrightest = "#D6E5FC" // Very light blue, near white + ColorSpinnerDim = "#9AB8F9" + ColorSpinnerBright = "#B8CFFB" + ColorSpinnerBrightest = "#D6E5FC" // Background colors - ColorBackground = "#1A1B26" // Dark blue-black + ColorBackground = "#1C1C22" // Status colors - ColorInfoCyan = "#7DCFFF" // Soft cyan + ColorInfoCyan = "#7DCFFF" + ColorHighlight = "#99f868" // Badge colors - ColorAgentBadge = "#BB9AF7" // Soft purple - ColorTransferBadge = "#7DCFFF" // Soft cyan + ColorAgentBadge = "#1D63ED" // Diff colors - ColorDiffAddBg = "#20303B" // Dark blue-green - ColorDiffRemoveBg = "#3C2A2A" // Dark red-brown + ColorDiffAddBg = "#20303B" + ColorDiffRemoveBg = "#3C2A2A" // Line number and UI element colors - ColorLineNumber = "#565F89" // Muted blue-grey (same as ColorMutedBlue) - ColorSeparator = "#414868" // Dark blue-grey (same as ColorBorderSecondary) + ColorLineNumber = "#565F89" + ColorSeparator = "#414868" // Interactive element colors - ColorSelected = "#364A82" // Dark blue for selected items - ColorHover = "#2D3F5F" // Slightly lighter than selected + ColorSelected = "#364A82" + ColorHover = "#2D3F5F" // AutoCompleteGhost colors ColorSuggestionGhost = "#6B6B6B" + + // Tab colors + ColorTab = "#25252c" ) // Chroma syntax highlighting colors (Monokai theme) @@ -101,27 +106,27 @@ var ( BackgroundAlt = lipgloss.Color(ColorBackgroundAlt) // Primary accent colors - Accent = lipgloss.Color(ColorAccentBlue) - AccentDim = lipgloss.Color(ColorMutedBlue) + White = lipgloss.Color(ColorWhite) + Accent = lipgloss.Color(ColorAccentBlue) // Status colors - softer, more professional - Success = lipgloss.Color(ColorSuccessGreen) - Error = lipgloss.Color(ColorErrorRed) - Warning = lipgloss.Color(ColorWarningYellow) - Info = lipgloss.Color(ColorInfoCyan) + Success = lipgloss.Color(ColorSuccessGreen) + Error = lipgloss.Color(ColorErrorRed) + Warning = lipgloss.Color(ColorWarningYellow) + Info = lipgloss.Color(ColorInfoCyan) + Highlight = lipgloss.Color(ColorHighlight) // Text hierarchy TextPrimary = lipgloss.Color(ColorTextPrimary) TextSecondary = lipgloss.Color(ColorTextSecondary) TextMuted = lipgloss.Color(ColorMutedBlue) - TextSubtle = lipgloss.Color(ColorBorderSecondary) + TextMutedGray = lipgloss.Color(ColorMutedGray) // Border colors BorderPrimary = lipgloss.Color(ColorAccentBlue) BorderSecondary = lipgloss.Color(ColorBorderSecondary) BorderMuted = lipgloss.Color(ColorBackgroundAlt) BorderWarning = lipgloss.Color(ColorWarningYellow) - BorderError = lipgloss.Color(ColorErrorRed) // Diff colors (matching glamour/markdown "dark" theme) DiffAddBg = lipgloss.Color(ColorDiffAddBg) @@ -136,30 +141,33 @@ var ( // Interactive element colors Selected = lipgloss.Color(ColorSelected) SelectedFg = lipgloss.Color(ColorTextPrimary) - Hover = lipgloss.Color(ColorHover) - PlaceholderColor = lipgloss.Color(ColorMutedBlue) + PlaceholderColor = lipgloss.Color(ColorMutedGray) // Badge colors - AgentBadge = lipgloss.Color(ColorAgentBadge) - TransferBadge = lipgloss.Color(ColorTransferBadge) + AgentBadgeFg = lipgloss.Color(ColorWhite) + AgentBadgeBg = lipgloss.Color(ColorAgentBadge) + + // Tabs + TabBg = lipgloss.Color(ColorTab) + TabPrimaryFg = lipgloss.Color(ColorMutedGray) + TabAccentFg = lipgloss.Color(ColorHighlight) ) // Base Styles const AppPaddingLeft = 1 // Keep in sync with AppStyle padding var ( - BaseStyle = lipgloss.NewStyle().Foreground(TextPrimary) + NoStyle = lipgloss.NewStyle() + BaseStyle = NoStyle.Foreground(TextPrimary) AppStyle = BaseStyle.Padding(0, 1, 0, AppPaddingLeft) ) // Text Styles var ( - HighlightStyle = BaseStyle.Foreground(Accent) - MutedStyle = BaseStyle.Foreground(TextMuted) - SubtleStyle = BaseStyle.Foreground(TextSubtle) - SecondaryStyle = BaseStyle.Foreground(TextSecondary) - BoldStyle = BaseStyle.Bold(true) - ItalicStyle = BaseStyle.Italic(true) + HighlightWhiteStyle = BaseStyle.Foreground(White).Bold(true) + MutedStyle = BaseStyle.Foreground(TextMutedGray) + SecondaryStyle = BaseStyle.Foreground(TextSecondary) + BoldStyle = BaseStyle.Bold(true) ) // Status Styles @@ -169,8 +177,9 @@ var ( WarningStyle = BaseStyle.Foreground(Warning) InfoStyle = BaseStyle.Foreground(Info) ActiveStyle = BaseStyle.Foreground(Success) - InProgressStyle = BaseStyle.Foreground(Warning) - PendingStyle = BaseStyle.Foreground(TextSecondary) + ToBeDoneStyle = BaseStyle.Foreground(TextPrimary) + InProgressStyle = BaseStyle.Foreground(Highlight) + CompletedStyle = BaseStyle.Foreground(TextMutedGray) ) // Layout Styles @@ -180,21 +189,7 @@ var ( // Border Styles var ( - BorderStyle = BaseStyle. - Border(lipgloss.RoundedBorder()). - BorderForeground(BorderPrimary) - - BorderedBoxStyle = BaseStyle. - Border(lipgloss.RoundedBorder()). - BorderForeground(BorderSecondary). - Padding(0, 1) - - BorderedBoxFocusedStyle = BaseStyle. - Border(lipgloss.RoundedBorder()). - BorderForeground(BorderPrimary). - Padding(0, 1) - - UserMessageBorderStyle = BaseStyle. + UserMessageStyle = BaseStyle. Padding(1, 2). BorderLeft(true). BorderStyle(lipgloss.ThickBorder()). @@ -202,11 +197,13 @@ var ( Bold(true). Background(BackgroundAlt) - WelcomeMessageBorderStyle = BaseStyle. - Padding(1, 2). - BorderLeft(true). - BorderStyle(lipgloss.DoubleBorder()). - Bold(true) + AssistantMessageStyle = BaseStyle + + WelcomeMessageStyle = BaseStyle. + Padding(1, 2). + BorderLeft(true). + BorderStyle(lipgloss.DoubleBorder()). + Bold(true) ErrorMessageStyle = ErrorStyle. Padding(0, 2). @@ -251,14 +248,6 @@ var ( DialogSeparatorStyle = BaseStyle. Foreground(BorderMuted) - DialogLabelStyle = BaseStyle. - Bold(true). - Foreground(TextMuted) - - DialogValueStyle = BaseStyle. - Bold(true). - Foreground(TextSecondary) - DialogQuestionStyle = BaseStyle. Bold(true). Foreground(TextPrimary). @@ -271,6 +260,19 @@ var ( DialogHelpStyle = BaseStyle. Foreground(TextMuted). Italic(true) + + TabTitleStyle = BaseStyle. + Foreground(TabPrimaryFg) + + TabStyle = TabPrimaryStyle. + Padding(1, 0) + + TabPrimaryStyle = BaseStyle. + Foreground(TextPrimary) + + TabAccentStyle = BaseStyle. + Foreground(TabAccentFg). + Background(TabBg) ) // Command Palette Styles @@ -288,9 +290,6 @@ var ( Bold(true). Foreground(TextMuted). MarginTop(1) - - PaletteDescStyle = BaseStyle. - Foreground(TextMuted) ) // Diff Styles (matching glamour markdown theme) @@ -304,8 +303,6 @@ var ( Foreground(DiffRemoveFg) DiffUnchangedStyle = BaseStyle.Background(BackgroundAlt) - - DiffContextStyle = BaseStyle ) // Syntax highlighting UI element styles @@ -317,20 +314,21 @@ var ( // Tool Call Styles var ( ToolMessageStyle = BaseStyle. - Padding(1). - BorderLeft(true). - BorderStyle(lipgloss.ThickBorder()). - BorderForeground(BorderSecondary). - Background(BackgroundAlt) + Foreground(TextMutedGray) - ToolCallArgs = BaseStyle - ToolCallResult = BaseStyle + ToolCallArgs = ToolMessageStyle. + Padding(0, 0, 0, 2) - ToolCallArgKey = BaseStyle.Bold(true).Foreground(TextSecondary) + ToolCallResult = ToolMessageStyle. + Padding(0, 0, 0, 2) + + ToolCallArgKey = BaseStyle. + Bold(true). + Foreground(TextMutedGray) ToolCallResultKey = BaseStyle. Bold(true). - Foreground(TextSecondary) + Foreground(TextMutedGray) ) // Input Styles @@ -348,7 +346,7 @@ var ( Color: Accent, }, } - EditorStyle = BaseStyle.Padding(1, 0, 1, 0) + EditorStyle = BaseStyle.Padding(1, 0, 0, 0) // SuggestionGhostStyle renders inline auto-complete hints in a muted tone. // Use a distinct grey so suggestion text is visually separate from the user's input. SuggestionGhostStyle = BaseStyle.Foreground(lipgloss.Color(ColorSuggestionGhost)) @@ -437,21 +435,15 @@ var ( // Agent and transfer badge styles var ( AgentBadgeStyle = BaseStyle. - Foreground(AgentBadge). - Bold(true). - Padding(0, 1) - - TransferBadgeStyle = BaseStyle. - Foreground(TransferBadge). - Bold(true). - Padding(0, 1) + Foreground(AgentBadgeFg). + Background(AgentBadgeBg). + Bold(true). + Padding(0, 1, 0, 1) ) // Deprecated styles (kept for backward compatibility) var ( - StatusStyle = MutedStyle - ActionStyle = SecondaryStyle - ChatStyle = BaseStyle + ChatStyle = BaseStyle ) // Selection Styles @@ -535,10 +527,10 @@ func ChromaStyle() *chroma.Style { func MarkdownStyle() ansi.StyleConfig { h1Color := ColorAccentBlue h2Color := ColorAccentBlue - h3Color := ColorTextSecondary - h4Color := ColorTextSecondary - h5Color := ColorTextSecondary - h6Color := ColorMutedBlue + h3Color := ColorAccentBlue + h4Color := ColorAccentBlue + h5Color := ColorAccentBlue + h6Color := ColorAccentBlue linkColor := ColorAccentBlue strongColor := ColorTextPrimary codeColor := ColorTextPrimary @@ -576,11 +568,9 @@ func MarkdownStyle() ansi.StyleConfig { }, H1: ansi.StyleBlock{ StylePrimitive: ansi.StylePrimitive{ - Prefix: " ", - Suffix: " ", - Color: &h1Color, - BackgroundColor: stringPtr(ANSIColor63), - Bold: boolPtr(true), + Prefix: "## ", + Color: &h1Color, + Bold: boolPtr(true), }, }, H2: ansi.StyleBlock{ @@ -611,7 +601,6 @@ func MarkdownStyle() ansi.StyleConfig { StylePrimitive: ansi.StylePrimitive{ Prefix: "###### ", Color: &h6Color, - Bold: boolPtr(false), }, }, Strikethrough: ansi.StylePrimitive{ diff --git a/pkg/tui/tui.go b/pkg/tui/tui.go index f34a1d276..b08f800af 100644 --- a/pkg/tui/tui.go +++ b/pkg/tui/tui.go @@ -61,7 +61,7 @@ func DefaultKeyMap() KeyMap { return KeyMap{ CommandPalette: key.NewBinding( key.WithKeys("ctrl+p"), - key.WithHelp("ctrl+p", "commands"), + key.WithHelp("Ctrl+p", "commands"), ), } } @@ -403,7 +403,7 @@ func (a *appModel) View() tea.View { styles.CenterStyle. Width(a.wWidth). Height(a.wHeight). - Render(styles.MutedStyle.Render("Loading...")), + Render(styles.MutedStyle.Render("Loading…")), ) }